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, LeafId, SplitDirection, SplitId};
32use crate::services::terminal::TerminalId;
33use crate::state::ViewMode;
34use crate::view::split::{SplitNode, SplitViewState};
35use crate::workspace::{
36 FileExplorerState, PersistedFileWorkspace, SearchOptions, SerializedBookmark, SerializedCursor,
37 SerializedFileState, SerializedFoldRange, SerializedScroll, SerializedSplitDirection,
38 SerializedSplitNode, SerializedSplitViewState, SerializedTabRef, SerializedTerminalWorkspace,
39 SerializedViewMode, UnnamedBufferRef, Workspace, WorkspaceConfigOverrides, WorkspaceError,
40 WorkspaceHistories, WORKSPACE_VERSION,
41};
42
43use super::bookmarks::{Bookmark, BookmarkState};
44use super::Editor;
45
46fn resolve_fold_header_line(
58 buffer: &crate::model::buffer::Buffer,
59 saved_line: usize,
60 header_text: Option<&str>,
61) -> Option<usize> {
62 let Some(expected) = header_text else {
63 return Some(saved_line);
65 };
66 let expected_trimmed = expected.trim();
67 let line_matches = |line: usize| -> bool {
68 buffer
69 .get_line(line)
70 .map(|bytes| {
71 let text = String::from_utf8_lossy(&bytes);
72 text.trim_end_matches('\n').trim_end_matches('\r').trim() == expected_trimmed
73 })
74 .unwrap_or(false)
75 };
76 if line_matches(saved_line) {
77 return Some(saved_line);
78 }
79 const SEARCH_WINDOW: usize = 32;
81 for delta in 1..=SEARCH_WINDOW {
82 let above = saved_line.checked_sub(delta);
83 if let Some(l) = above {
84 if line_matches(l) {
85 return Some(l);
86 }
87 }
88 let below = saved_line.saturating_add(delta);
89 if line_matches(below) {
90 return Some(below);
91 }
92 }
93 None
94}
95
96pub struct WorkspaceTracker {
100 dirty: bool,
102 last_save: Instant,
104 save_interval: std::time::Duration,
106 enabled: bool,
108}
109
110impl WorkspaceTracker {
111 pub fn new(enabled: bool) -> Self {
113 Self {
114 dirty: false,
115 last_save: Instant::now(),
116 save_interval: std::time::Duration::from_secs(5),
117 enabled,
118 }
119 }
120
121 pub fn is_enabled(&self) -> bool {
123 self.enabled
124 }
125
126 pub fn mark_dirty(&mut self) {
128 if self.enabled {
129 self.dirty = true;
130 }
131 }
132
133 pub fn should_save(&self) -> bool {
135 self.enabled && self.dirty && self.last_save.elapsed() >= self.save_interval
136 }
137
138 pub fn record_save(&mut self) {
140 self.dirty = false;
141 self.last_save = Instant::now();
142 }
143
144 pub fn is_dirty(&self) -> bool {
146 self.dirty
147 }
148}
149
150impl Editor {
151 pub fn capture_workspace(&self) -> Workspace {
157 let mut ws = self.active_window().capture_workspace();
158 ws.plugin_global_state = self.plugin_global_state.clone();
159 ws
160 }
161
162 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
165 self.save_workspace_for(self.active_window)
166 }
167
168 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
173 self.restore_workspace_for(self.active_window)
174 }
175
176 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
182 if !self.config.editor.hot_exit {
183 return Ok(0);
184 }
185
186 let entries = self.recovery_service.lock().unwrap().list_recoverable()?;
187 if entries.is_empty() {
188 return Ok(0);
189 }
190
191 let buffer_files: Vec<_> = self
193 .buffers()
194 .iter()
195 .filter_map(|(buffer_id, state)| {
196 let path = state.buffer.file_path()?.to_path_buf();
197 if path.as_os_str().is_empty() {
198 return None; }
200 Some((*buffer_id, path))
201 })
202 .collect();
203
204 let mut recovered = 0;
205 for (buffer_id, file_path) in buffer_files {
206 let recovery_id = self
207 .recovery_service
208 .lock()
209 .unwrap()
210 .get_buffer_id(Some(&file_path));
211 let entry = entries.iter().find(|e| e.id == recovery_id);
212 if let Some(entry) = entry {
213 let loaded = self.recovery_service.lock().unwrap().load_recovery(entry);
214 match loaded {
215 Ok(crate::services::recovery::RecoveryResult::Recovered {
216 content, ..
217 }) => {
218 let mut mutated = false;
219 if let Some(state) = self
220 .windows
221 .get_mut(&self.active_window)
222 .map(|w| &mut w.buffers)
223 .expect("active window present")
224 .get_mut(&buffer_id)
225 {
226 let current_len = state.buffer.total_bytes();
227 let text = String::from_utf8_lossy(&content).into_owned();
228 let current = state.buffer.get_text_range_mut(0, current_len).ok();
229 let current_text = current
230 .as_ref()
231 .map(|b| String::from_utf8_lossy(b).into_owned());
232 if current_text.as_deref() != Some(&text) {
233 state.buffer.delete(0..current_len);
234 state.buffer.insert(0, &text);
235 state.buffer.set_modified(true);
236 state.buffer.set_recovery_pending(false);
237 if let Some(log) =
240 self.active_window_mut().event_logs.get_mut(&buffer_id)
241 {
242 log.clear_saved_position();
243 }
244 mutated = true;
245 recovered += 1;
246 tracing::info!(
247 "Restored unsaved changes for {:?} from hot exit recovery",
248 file_path
249 );
250 }
251 }
252 if mutated {
253 self.sync_lsp_after_recovery_replay(buffer_id);
254 }
255 }
256 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
257 chunks,
258 ..
259 }) => {
260 let mut mutated = false;
261 if let Some(state) = self
262 .windows
263 .get_mut(&self.active_window)
264 .map(|w| &mut w.buffers)
265 .expect("active window present")
266 .get_mut(&buffer_id)
267 {
268 for chunk in chunks.into_iter().rev() {
269 let text = String::from_utf8_lossy(&chunk.content).into_owned();
270 if chunk.original_len > 0 {
271 state
272 .buffer
273 .delete(chunk.offset..chunk.offset + chunk.original_len);
274 }
275 state.buffer.insert(chunk.offset, &text);
276 }
277 state.buffer.set_modified(true);
278 state.buffer.set_recovery_pending(false);
279 if let Some(log) =
282 self.active_window_mut().event_logs.get_mut(&buffer_id)
283 {
284 log.clear_saved_position();
285 }
286 mutated = true;
287 recovered += 1;
288 tracing::info!(
289 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
290 file_path
291 );
292 }
293 if mutated {
294 self.sync_lsp_after_recovery_replay(buffer_id);
295 }
296 }
297 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
298 original_path,
299 ..
300 }) => {
301 let name = original_path
302 .file_name()
303 .unwrap_or_default()
304 .to_string_lossy();
305 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
306 self.set_status_message(format!(
307 "{} changed on disk; unsaved changes not restored",
308 name
309 ));
310 }
311 Ok(_) => {} Err(e) => {
313 tracing::debug!(
314 "Failed to load hot exit recovery for {:?}: {}",
315 file_path,
316 e
317 );
318 }
319 }
320 }
321 }
322
323 Ok(recovered)
324 }
325
326 fn restore_config_overrides(&mut self, overrides: &WorkspaceConfigOverrides) {
331 if let Some(line_numbers) = overrides.line_numbers {
332 self.config_mut().editor.line_numbers = line_numbers;
333 }
334 if let Some(relative_line_numbers) = overrides.relative_line_numbers {
335 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
336 }
337 if let Some(line_wrap) = overrides.line_wrap {
338 self.config_mut().editor.line_wrap = line_wrap;
339 }
340 if let Some(syntax_highlighting) = overrides.syntax_highlighting {
341 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
342 }
343 if let Some(enable_inlay_hints) = overrides.enable_inlay_hints {
344 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
345 }
346 }
351
352 pub fn save_workspace_for(&mut self, id: fresh_core::WindowId) -> Result<(), WorkspaceError> {
357 let Some(win) = self.windows.get(&id) else {
358 return Ok(());
359 };
360
361 win.sync_terminal_backing_files();
364 win.save_all_global_file_states();
365
366 let mut workspace = win.capture_workspace();
367 workspace.plugin_global_state = self.plugin_global_state.clone();
368
369 if workspace.has_no_real_content() && win.has_any_virtual_buffer() {
374 let root = win.root.clone();
375 let on_disk = if let Some(ref session_name) = self.session_name {
376 Workspace::load_session(session_name, &root).ok().flatten()
377 } else {
378 Workspace::load(&root).ok().flatten()
379 };
380 if let Some(existing) = on_disk {
381 if !existing.has_no_preservable_content() {
382 tracing::info!(
383 "Skipping workspace save: only virtual buffers are open, \
384 on-disk workspace already has preservable file content"
385 );
386 return Ok(());
387 }
388 }
389 }
390
391 if let Some(ref session_name) = self.session_name {
393 workspace.save_session(session_name)
394 } else {
395 workspace.save()
396 }
397 }
398
399 pub fn restore_workspace_for(
411 &mut self,
412 id: fresh_core::WindowId,
413 ) -> Result<bool, WorkspaceError> {
414 let Some(root) = self.windows.get(&id).map(|w| w.root.clone()) else {
415 return Ok(false);
416 };
417
418 let workspace = if let Some(ref session_name) = self.session_name {
419 Workspace::load_session(session_name, &root)?
420 } else {
421 Workspace::load(&root)?
422 };
423 let Some(workspace) = workspace else {
424 tracing::debug!("No workspace found for {:?}", root);
425 return Ok(false);
426 };
427 tracing::info!("Found workspace for {:?}, applying...", root);
428
429 self.restore_config_overrides(&workspace.config_overrides);
431 if !workspace.plugin_global_state.is_empty() {
432 tracing::debug!(
433 "Restoring plugin global state for {} plugins",
434 workspace.plugin_global_state.len()
435 );
436 self.plugin_global_state = workspace.plugin_global_state.clone();
437 }
438
439 let populated = self
440 .windows
441 .get(&id)
442 .map(|w| w.buffers.splits().is_some() && w.buffers.len() > 0)
443 .unwrap_or(false);
444
445 let session = self.session_name.clone();
446 if populated {
447 let win = self
450 .windows
451 .get_mut(&id)
452 .expect("window present for restore");
453 win.apply_workspace_layout(&workspace, session.as_deref());
454 } else {
455 let (label, root2, resources, tw, th, pstate) = {
459 let w = self.windows.get(&id).expect("window present for restore");
460 (
461 w.label.clone(),
462 w.root.clone(),
463 w.resources.clone(),
464 w.terminal_width,
465 w.terminal_height,
466 w.plugin_state.clone(),
467 )
468 };
469 let mut built =
470 crate::app::window::Window::from_workspace(id, label, root2, resources, &workspace);
471 built.terminal_width = tw;
472 built.terminal_height = th;
473 built.plugin_state = pstate;
474 self.windows.insert(id, built);
475 }
476
477 if id == self.active_window {
481 #[cfg(feature = "plugins")]
482 {
483 let buffer_id = self.active_buffer();
484 self.update_plugin_state_snapshot();
485 tracing::debug!(
486 "Firing buffer_activated for active buffer {:?} after workspace restore",
487 buffer_id
488 );
489 self.plugin_manager.read().unwrap().run_hook(
490 "buffer_activated",
491 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
492 );
493 }
494 }
495
496 Ok(true)
497 }
498
499 pub fn save_all_windows_workspaces(&mut self) -> Result<(), WorkspaceError> {
506 let targets: Vec<fresh_core::WindowId> = self
507 .windows
508 .iter()
509 .filter(|(_, w)| w.buffers.splits().is_some())
510 .map(|(id, _)| *id)
511 .collect();
512
513 let mut first_err = None;
514 for id in targets {
515 if let Err(e) = self.save_workspace_for(id) {
516 tracing::warn!("Failed to save workspace for window {id}: {e}");
517 if first_err.is_none() {
518 first_err = Some(e);
519 }
520 }
521 }
522
523 match first_err {
524 Some(e) => Err(e),
525 None => Ok(()),
526 }
527 }
528
529 pub fn restore_inactive_window_workspaces(&mut self) {
540 let active = self.active_window;
541 let saved_plugin_state = self.plugin_global_state.clone();
542
543 let targets: Vec<fresh_core::WindowId> = self
544 .windows
545 .keys()
546 .copied()
547 .filter(|id| *id != active)
548 .collect();
549
550 for id in targets {
551 match self.restore_workspace_for(id) {
552 Ok(true) => tracing::debug!("Restored workspace for inactive window {id}"),
553 Ok(false) => tracing::trace!(
554 "No persisted workspace for inactive window {id}; seed layout kept"
555 ),
556 Err(e) => {
557 tracing::warn!("Failed to restore workspace for inactive window {id}: {e}")
558 }
559 }
560 }
561
562 self.plugin_global_state = saved_plugin_state;
563 }
564}
565
566impl crate::app::window::Window {
567 fn restore_terminals_from_workspace(
568 &mut self,
569 terminals: &[SerializedTerminalWorkspace],
570 ) -> HashMap<usize, BufferId> {
571 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
572 if terminals.is_empty() {
573 return terminal_buffer_map;
574 }
575 let __window_bridge = self.bridge.clone();
576 self.terminal_manager.set_async_bridge(__window_bridge);
577 for terminal in terminals {
578 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
579 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
580 }
581 }
582 terminal_buffer_map
583 }
584
585 fn restore_bookmarks_from_workspace(
587 &mut self,
588 bookmarks: &HashMap<char, SerializedBookmark>,
589 path_to_buffer: &HashMap<PathBuf, BufferId>,
590 ) {
591 for (key, bookmark) in bookmarks {
592 let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) else {
593 continue;
594 };
595 if let Some(buffer) = self.buffers.get(&buffer_id) {
596 let pos = bookmark.position.min(buffer.buffer.len());
597 self.bookmarks.set(
598 *key,
599 Bookmark {
600 buffer_id,
601 position: pos,
602 },
603 );
604 }
605 }
606 }
607
608 fn clean_orphaned_buffers(&mut self) {
611 let referenced: HashSet<BufferId> = self
612 .buffers
613 .splits()
614 .map(|(_, vs)| vs)
615 .expect("active window must have a populated split layout")
616 .values()
617 .flat_map(|vs| vs.buffer_tab_ids())
618 .collect();
619 let orphans: Vec<BufferId> = self
620 .buffers
621 .iter()
622 .filter(|(id, state)| {
623 !referenced.contains(id)
624 && state.buffer.file_path().is_none()
625 && !state.buffer.is_modified()
626 })
627 .map(|(id, _)| *id)
628 .collect();
629 for id in orphans {
630 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
631 self.buffers.remove(&id);
632 self.event_logs.remove(&id);
633 self.buffer_metadata.remove(&id);
634 }
635 }
636
637 fn log_restore_summary(&mut self, session_name: Option<&str>) {
640 tracing::debug!(
641 "Workspace restore complete: {} splits, {} buffers",
642 self.buffers
643 .splits()
644 .map(|(_, vs)| vs)
645 .expect("active window must have a populated split layout")
646 .len(),
647 self.buffers.len()
648 );
649 let restored_count = self.buffers.count_where(|id, _| {
650 self.buffer_metadata
651 .get(&id)
652 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
653 });
654 if restored_count == 0 {
655 return;
656 }
657 let msg = match session_name.map(|n| format!("session '{}'", n)) {
658 Some(label) => format!("Restored {} ({} buffer(s))", label, restored_count),
659 None => format!(
660 "Restored {} buffer(s) from previous session",
661 restored_count
662 ),
663 };
664 self.set_status_message(msg);
665 }
666
667 fn restore_terminal_from_workspace(
676 &mut self,
677 terminal: &SerializedTerminalWorkspace,
678 ) -> Option<BufferId> {
679 let terminals_root = self
681 .resources
682 .dir_context
683 .terminal_dir_for(self.root.as_path());
684 let log_path = if terminal.log_path.is_absolute() {
685 terminal.log_path.clone()
686 } else {
687 terminals_root.join(&terminal.log_path)
688 };
689 let backing_path = if terminal.backing_path.is_absolute() {
690 terminal.backing_path.clone()
691 } else {
692 terminals_root.join(&terminal.backing_path)
693 };
694
695 #[allow(clippy::let_underscore_must_use)]
697 let _ = self.resources.authority.filesystem.create_dir_all(
698 log_path
699 .parent()
700 .or_else(|| backing_path.parent())
701 .unwrap_or(&terminals_root),
702 );
703
704 let predicted_id = self.terminal_manager.next_terminal_id();
706 self.terminal_log_files
707 .insert(predicted_id, log_path.clone());
708 self.terminal_backing_files
709 .insert(predicted_id, backing_path.clone());
710
711 let wrapper_for_spawn = self.resolved_terminal_wrapper();
713 let terminal_id = match self.terminal_manager.spawn(
714 terminal.cols,
715 terminal.rows,
716 terminal.cwd.clone(),
717 Some(log_path.clone()),
718 Some(backing_path.clone()),
719 wrapper_for_spawn,
720 ) {
721 Ok(id) => id,
722 Err(e) => {
723 tracing::warn!(
724 "Failed to restore terminal {}: {}",
725 terminal.terminal_index,
726 e
727 );
728 return None;
729 }
730 };
731
732 if terminal_id != predicted_id {
734 self.terminal_log_files
735 .insert(terminal_id, log_path.clone());
736 self.terminal_backing_files
737 .insert(terminal_id, backing_path.clone());
738 self.terminal_log_files.remove(&predicted_id);
739 self.terminal_backing_files.remove(&predicted_id);
740 }
741
742 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
744
745 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
748
749 Some(buffer_id)
750 }
751
752 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
757 if !backing_path.exists() {
759 return;
760 }
761
762 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
763 if let Ok(new_state) = EditorState::from_file_with_languages(
764 backing_path,
765 self.terminal_width,
766 self.terminal_height,
767 large_file_threshold,
768 &self.resources.grammar_registry,
769 &self.resources.config.languages,
770 std::sync::Arc::clone(&self.resources.authority.filesystem),
771 ) {
772 self.install_terminal_buffer_state(buffer_id, new_state);
773 }
774 }
775
776 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
778 for (buffer_id, metadata) in &self.buffer_metadata {
780 if let Some(file_path) = metadata.file_path() {
781 if file_path == path {
782 return Ok(*buffer_id);
783 }
784 }
785 }
786
787 self.open_file_no_focus(path).map_err(WorkspaceError::Io)
789 }
790
791 #[allow(clippy::too_many_arguments)]
793 fn restore_split_node(
794 &mut self,
795 node: &SerializedSplitNode,
796 path_to_buffer: &HashMap<PathBuf, BufferId>,
797 terminal_buffers: &HashMap<usize, BufferId>,
798 unnamed_buffers: &HashMap<String, BufferId>,
799 split_states: &HashMap<usize, SerializedSplitViewState>,
800 split_id_map: &mut HashMap<usize, SplitId>,
801 is_first_leaf: bool,
802 ) {
803 match node {
804 SerializedSplitNode::Leaf {
805 file_path,
806 split_id,
807 label,
808 unnamed_recovery_id,
809 role,
810 } => {
811 let buffer_id = file_path
813 .as_ref()
814 .and_then(|p| path_to_buffer.get(p).copied())
815 .or_else(|| {
816 unnamed_recovery_id
817 .as_ref()
818 .and_then(|id| unnamed_buffers.get(id).copied())
819 })
820 .unwrap_or(self.active_buffer());
821
822 let current_leaf_id = if is_first_leaf {
823 let leaf_id = self
825 .buffers
826 .splits()
827 .map(|(mgr, _)| mgr)
828 .expect("active window must have a populated split layout")
829 .active_split();
830 self.set_pane_buffer(leaf_id, buffer_id);
831 leaf_id
832 } else {
833 self.buffers
835 .splits()
836 .map(|(mgr, _)| mgr)
837 .expect("active window must have a populated split layout")
838 .active_split()
839 };
840
841 split_id_map.insert(*split_id, current_leaf_id.into());
843
844 if let Some(label) = label {
846 self.buffers
847 .split_manager_mut()
848 .expect("active window must have a populated split layout")
849 .set_label(current_leaf_id, label.clone());
850 }
851
852 if let Some(role) = role {
855 self.buffers
856 .split_manager_mut()
857 .expect("active window must have a populated split layout")
858 .clear_role(*role);
859 self.buffers
860 .split_manager_mut()
861 .expect("active window must have a populated split layout")
862 .set_leaf_role(current_leaf_id, Some(*role));
863 }
864
865 self.restore_split_view_state(
867 current_leaf_id,
868 *split_id,
869 split_states,
870 path_to_buffer,
871 terminal_buffers,
872 unnamed_buffers,
873 );
874 }
875 SerializedSplitNode::Terminal {
876 terminal_index,
877 split_id,
878 label,
879 role,
880 } => {
881 let buffer_id = terminal_buffers
882 .get(terminal_index)
883 .copied()
884 .unwrap_or(self.active_buffer());
885
886 let current_leaf_id = if is_first_leaf {
887 let leaf_id = self
888 .buffers
889 .splits()
890 .map(|(mgr, _)| mgr)
891 .expect("active window must have a populated split layout")
892 .active_split();
893 self.set_pane_buffer(leaf_id, buffer_id);
894 leaf_id
895 } else {
896 self.buffers
897 .splits()
898 .map(|(mgr, _)| mgr)
899 .expect("active window must have a populated split layout")
900 .active_split()
901 };
902
903 split_id_map.insert(*split_id, current_leaf_id.into());
904
905 if let Some(label) = label {
907 self.buffers
908 .split_manager_mut()
909 .expect("active window must have a populated split layout")
910 .set_label(current_leaf_id, label.clone());
911 }
912
913 if let Some(role) = role {
916 self.buffers
917 .split_manager_mut()
918 .expect("active window must have a populated split layout")
919 .clear_role(*role);
920 self.buffers
921 .split_manager_mut()
922 .expect("active window must have a populated split layout")
923 .set_leaf_role(current_leaf_id, Some(*role));
924 }
925
926 self.buffers
927 .split_manager_mut()
928 .expect("active window must have a populated split layout")
929 .set_split_buffer(current_leaf_id, buffer_id);
930
931 self.restore_split_view_state(
932 current_leaf_id,
933 *split_id,
934 split_states,
935 path_to_buffer,
936 terminal_buffers,
937 unnamed_buffers,
938 );
939 }
940 SerializedSplitNode::Split {
941 direction,
942 first,
943 second,
944 ratio,
945 split_id,
946 } => {
947 self.restore_split_node(
949 first,
950 path_to_buffer,
951 terminal_buffers,
952 unnamed_buffers,
953 split_states,
954 split_id_map,
955 is_first_leaf,
956 );
957
958 let second_buffer_id = get_first_leaf_buffer(
960 second,
961 path_to_buffer,
962 terminal_buffers,
963 unnamed_buffers,
964 )
965 .unwrap_or(self.active_buffer());
966
967 let split_direction = match direction {
969 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
970 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
971 };
972
973 match self
975 .buffers
976 .split_manager_mut()
977 .expect("active window must have a populated split layout")
978 .split_active(split_direction, second_buffer_id, *ratio)
979 {
980 Ok(new_leaf_id) => {
981 let mut view_state = SplitViewState::with_buffer(
983 self.terminal_width,
984 self.terminal_height,
985 second_buffer_id,
986 );
987 view_state.apply_config_defaults(
988 self.resources.config.editor.line_numbers,
989 self.resources.config.editor.highlight_current_line,
990 self.resolve_line_wrap_for_buffer(second_buffer_id),
991 self.resources.config.editor.wrap_indent,
992 self.resolve_wrap_column_for_buffer(second_buffer_id),
993 self.resources.config.editor.rulers.clone(),
994 );
995 self.buffers
996 .split_view_states_mut()
997 .expect("active window must have a populated split layout")
998 .insert(new_leaf_id, view_state);
999
1000 split_id_map.insert(*split_id, new_leaf_id.into());
1002
1003 self.restore_split_node(
1005 second,
1006 path_to_buffer,
1007 terminal_buffers,
1008 unnamed_buffers,
1009 split_states,
1010 split_id_map,
1011 false,
1012 );
1013 }
1014 Err(e) => {
1015 tracing::error!("Failed to create split during workspace restore: {}", e);
1016 }
1017 }
1018 }
1019 }
1020 }
1021
1022 fn restore_split_view_state(
1024 &mut self,
1025 current_split_id: LeafId,
1026 saved_split_id: usize,
1027 split_states: &HashMap<usize, SerializedSplitViewState>,
1028 path_to_buffer: &HashMap<PathBuf, BufferId>,
1029 terminal_buffers: &HashMap<usize, BufferId>,
1030 unnamed_buffers: &HashMap<String, BufferId>,
1031 ) {
1032 let Some(split_state) = split_states.get(&saved_split_id) else {
1034 return;
1035 };
1036
1037 let split_buf_for_current = self
1041 .buffers
1042 .split_manager()
1043 .expect("active window must have a populated split layout")
1044 .buffer_for_split(current_split_id);
1045 let active_buffer_id = self
1046 .buffers
1047 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1048 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1049 return None;
1050 };
1051 let mut active_buffer_id: Option<BufferId> = None;
1052 if !split_state.open_tabs.is_empty() {
1053 view_state.open_buffers.clear();
1056
1057 for tab in &split_state.open_tabs {
1058 match tab {
1059 SerializedTabRef::File(rel_path) => {
1060 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1061 if !view_state.has_buffer(buffer_id) {
1062 view_state.add_buffer(buffer_id);
1063 }
1064 view_state.ensure_buffer_state(buffer_id);
1066 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1067 let buf_state =
1068 view_state.buffer_state_mut(buffer_id).unwrap();
1069 buf_state.viewport.line_wrap_enabled = false;
1070 buf_state.show_line_numbers = false;
1074 buf_state.highlight_current_line = false;
1075 }
1076 }
1077 }
1078 SerializedTabRef::Terminal(index) => {
1079 if let Some(&buffer_id) = terminal_buffers.get(index) {
1080 if !view_state.has_buffer(buffer_id) {
1081 view_state.add_buffer(buffer_id);
1082 }
1083 let buf_state = view_state.ensure_buffer_state(buffer_id);
1084 buf_state.viewport.line_wrap_enabled = false;
1085 buf_state.show_line_numbers = false;
1089 buf_state.highlight_current_line = false;
1090 }
1091 }
1092 SerializedTabRef::Unnamed(recovery_id) => {
1093 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1094 if !view_state.has_buffer(buffer_id) {
1095 view_state.add_buffer(buffer_id);
1096 }
1097 view_state.ensure_buffer_state(buffer_id);
1098 }
1099 }
1100 }
1101 }
1102
1103 if view_state.open_buffers.is_empty() {
1108 if let Some(buf) = split_buf_for_current {
1109 view_state.add_buffer(buf);
1110 view_state.ensure_buffer_state(buf);
1111 }
1112 }
1113
1114 if let Some(active_idx) = split_state.active_tab_index {
1115 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1116 active_buffer_id = match tab {
1117 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1118 SerializedTabRef::Terminal(index) => {
1119 terminal_buffers.get(index).copied()
1120 }
1121 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1122 };
1123 }
1124 }
1125 } else {
1126 for rel_path in &split_state.open_files {
1128 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1129 if !view_state.has_buffer(buffer_id) {
1130 view_state.add_buffer(buffer_id);
1131 }
1132 view_state.ensure_buffer_state(buffer_id);
1133 }
1134 }
1135
1136 let active_file_path =
1137 split_state.open_files.get(split_state.active_file_index);
1138 active_buffer_id =
1139 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1140 }
1141
1142 for (rel_path, file_state) in &split_state.file_states {
1144 let rel_str = rel_path.to_string_lossy();
1146 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1147 match unnamed_buffers.get(recovery_id).copied() {
1148 Some(id) => id,
1149 None => continue,
1150 }
1151 } else {
1152 match path_to_buffer.get(rel_path).copied() {
1153 Some(id) => id,
1154 None => continue,
1155 }
1156 };
1157 let max_pos = __buffers_mut
1158 .get(&buffer_id)
1159 .map(|b| b.buffer.len())
1160 .unwrap_or(0);
1161
1162 let buf_state = view_state.ensure_buffer_state(buffer_id);
1164
1165 let cursor_pos = file_state.cursor.position.min(max_pos);
1166 buf_state.cursors.primary_mut().position = cursor_pos;
1167 buf_state.cursors.primary_mut().anchor =
1168 file_state.cursor.anchor.map(|a| a.min(max_pos));
1169 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1170
1171 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1172 buf_state.viewport.top_view_line_offset =
1173 file_state.scroll.top_view_line_offset;
1174 buf_state.viewport.left_column = file_state.scroll.left_column;
1175 buf_state.viewport.set_skip_resize_sync();
1176
1177 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1185 super::navigation::reconcile_restored_buffer_view(
1186 buf_state,
1187 &mut state.buffer,
1188 );
1189 }
1190
1191 buf_state.view_mode = match file_state.view_mode {
1193 SerializedViewMode::Source => ViewMode::Source,
1194 SerializedViewMode::PageView => ViewMode::PageView,
1195 };
1196 buf_state.compose_width = file_state.compose_width;
1197 buf_state.plugin_state = file_state.plugin_state.clone();
1198 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1199 buf_state.folds.clear(&mut state.marker_list);
1200 for fold in &file_state.folds {
1201 let Some(resolved_header) = resolve_fold_header_line(
1208 &state.buffer,
1209 fold.header_line,
1210 fold.header_text.as_deref(),
1211 ) else {
1212 tracing::debug!(
1213 "Dropping stale fold: header_line={} no longer matches stored \
1214 header_text after external edit",
1215 fold.header_line,
1216 );
1217 continue;
1218 };
1219
1220 let shift = resolved_header as i64 - fold.header_line as i64;
1222 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1223 let start_line = resolved_header.saturating_add(1);
1224 let end_line = adjusted_end;
1225 if start_line > end_line {
1226 continue;
1227 }
1228 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1229 else {
1230 continue;
1231 };
1232 let end_byte = state
1233 .buffer
1234 .line_start_offset(end_line.saturating_add(1))
1235 .unwrap_or_else(|| state.buffer.len());
1236 buf_state.folds.add(
1237 &mut state.marker_list,
1238 start_byte,
1239 end_byte,
1240 fold.placeholder.clone(),
1241 );
1242 }
1243 }
1244
1245 tracing::trace!(
1246 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1247 rel_path,
1248 cursor_pos,
1249 buf_state.viewport.top_byte,
1250 buf_state.view_mode,
1251 );
1252 }
1253
1254 let restored_view_mode = match split_state.view_mode {
1257 SerializedViewMode::Source => ViewMode::Source,
1258 SerializedViewMode::PageView => ViewMode::PageView,
1259 };
1260
1261 if let Some(active_buf_id) = active_buffer_id {
1262 view_state.switch_buffer(active_buf_id);
1264
1265 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
1267 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
1268 });
1269 if !active_has_file_state {
1270 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1271 view_state.active_state_mut().compose_width = split_state.compose_width;
1272 }
1273
1274 }
1276 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1277 active_buffer_id
1278 })
1279 .flatten();
1280
1281 if let Some(active_buf_id) = active_buffer_id {
1285 self.buffers
1286 .split_manager_mut()
1287 .expect("active window must have a populated split layout")
1288 .set_split_buffer(current_split_id, active_buf_id);
1289 }
1290 }
1291
1292 fn restore_search_options(&mut self, opts: &SearchOptions) {
1293 self.search_case_sensitive = opts.case_sensitive;
1294 self.search_whole_word = opts.whole_word;
1295 self.search_use_regex = opts.use_regex;
1296 self.search_confirm_each = opts.confirm_each;
1297 }
1298
1299 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
1300 tracing::debug!(
1301 "Restoring histories: {} search, {} replace, {} goto_line",
1302 histories.search.len(),
1303 histories.replace.len(),
1304 histories.goto_line.len()
1305 );
1306 for item in &histories.search {
1307 self.prompt_histories
1308 .entry("search".to_string())
1309 .or_default()
1310 .push(item.clone());
1311 }
1312 for item in &histories.replace {
1313 self.prompt_histories
1314 .entry("replace".to_string())
1315 .or_default()
1316 .push(item.clone());
1317 }
1318 for item in &histories.goto_line {
1319 self.prompt_histories
1320 .entry("goto_line".to_string())
1321 .or_default()
1322 .push(item.clone());
1323 }
1324 }
1325
1326 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
1327 self.file_explorer_visible = fe.visible;
1328 self.file_explorer_width = fe.width;
1329 self.file_explorer_side = fe.side;
1330
1331 if fe.show_hidden {
1333 self.pending_file_explorer_show_hidden = Some(true);
1334 }
1335 if fe.show_gitignored {
1336 self.pending_file_explorer_show_gitignored = Some(true);
1337 }
1338
1339 if self.file_explorer_visible && self.file_explorer.is_none() {
1341 self.init_file_explorer();
1342 }
1343 }
1344
1345 fn open_workspace_files(
1348 &mut self,
1349 split_states: &HashMap<usize, SerializedSplitViewState>,
1350 ) -> HashMap<PathBuf, BufferId> {
1351 let file_paths = collect_file_paths_from_states(split_states);
1352 tracing::debug!(
1353 "Workspace has {} files to restore: {:?}",
1354 file_paths.len(),
1355 file_paths
1356 );
1357 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
1358 for rel_path in file_paths {
1359 let abs_path = self.root.join(&rel_path);
1360 tracing::trace!(
1361 "Checking file: {:?} (exists: {})",
1362 abs_path,
1363 abs_path.exists()
1364 );
1365 if abs_path.exists() {
1366 match self.open_file_internal(&abs_path) {
1367 Ok(buffer_id) => {
1368 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
1369 path_to_buffer.insert(rel_path, buffer_id);
1370 }
1371 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
1372 }
1373 } else {
1374 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
1375 }
1376 }
1377 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
1378 path_to_buffer
1379 }
1380
1381 fn restore_external_files(
1383 &mut self,
1384 external_files: &[PathBuf],
1385 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
1386 ) {
1387 if external_files.is_empty() {
1388 return;
1389 }
1390 tracing::debug!(
1391 "Restoring {} external files: {:?}",
1392 external_files.len(),
1393 external_files
1394 );
1395 for abs_path in external_files {
1396 if !abs_path.exists() {
1397 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
1398 continue;
1399 }
1400 match self.open_file_internal(abs_path) {
1401 Ok(buffer_id) => {
1402 path_to_buffer.insert(abs_path.clone(), buffer_id);
1403 tracing::debug!(
1404 "Restored external file {:?} as buffer {:?}",
1405 abs_path,
1406 buffer_id
1407 );
1408 }
1409 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1410 }
1411 }
1412 }
1413
1414 fn apply_read_only_flags(
1417 &mut self,
1418 read_only_files: &[PathBuf],
1419 path_to_buffer: &HashMap<PathBuf, BufferId>,
1420 ) {
1421 for ro_path in read_only_files {
1422 let buffer_id = path_to_buffer
1423 .get(ro_path)
1424 .copied()
1425 .or_else(|| path_to_buffer.get(&self.root.join(ro_path)).copied());
1426 if let Some(id) = buffer_id {
1427 self.mark_buffer_read_only(id, true);
1428 }
1429 }
1430 }
1431
1432 pub(crate) fn has_any_virtual_buffer(&self) -> bool {
1437 self.buffer_metadata
1438 .values()
1439 .any(|m| matches!(m.kind, crate::app::types::BufferKind::Virtual { .. }))
1440 }
1441
1442 pub(crate) fn save_all_global_file_states(&self) {
1445 for (leaf_id, view_state) in self
1446 .buffers
1447 .splits()
1448 .map(|(_, vs)| vs)
1449 .expect("window must have a populated split layout")
1450 {
1451 let active_buffer = self
1452 .buffers
1453 .splits()
1454 .map(|(mgr, _)| mgr)
1455 .expect("window must have a populated split layout")
1456 .root()
1457 .get_leaves_with_rects(ratatui::layout::Rect::default())
1458 .into_iter()
1459 .find(|(sid, _, _)| *sid == *leaf_id)
1460 .map(|(_, buffer_id, _)| buffer_id);
1461
1462 if let Some(buffer_id) = active_buffer {
1463 self.save_buffer_file_state(buffer_id, view_state);
1464 }
1465 }
1466 }
1467
1468 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
1470 let abs_path = match self.buffer_metadata.get(&buffer_id) {
1471 Some(metadata) => match metadata.file_path() {
1472 Some(path) => path.to_path_buf(),
1473 None => return,
1474 },
1475 None => return,
1476 };
1477
1478 let primary_cursor = view_state.cursors.primary();
1479 let file_state = SerializedFileState {
1480 cursor: SerializedCursor {
1481 position: primary_cursor.position,
1482 anchor: primary_cursor.anchor,
1483 sticky_column: primary_cursor.sticky_column,
1484 },
1485 additional_cursors: view_state
1486 .cursors
1487 .iter()
1488 .skip(1)
1489 .map(|(_, cursor)| SerializedCursor {
1490 position: cursor.position,
1491 anchor: cursor.anchor,
1492 sticky_column: cursor.sticky_column,
1493 })
1494 .collect(),
1495 scroll: SerializedScroll {
1496 top_byte: view_state.viewport.top_byte,
1497 top_view_line_offset: view_state.viewport.top_view_line_offset,
1498 left_column: view_state.viewport.left_column,
1499 },
1500 view_mode: Default::default(),
1501 compose_width: None,
1502 plugin_state: std::collections::HashMap::new(),
1503 folds: Vec::new(),
1504 };
1505
1506 PersistedFileWorkspace::save(&abs_path, file_state);
1507 }
1508
1509 pub(crate) fn sync_terminal_backing_files(&self) {
1512 use std::io::BufWriter;
1513
1514 let terminals_to_sync: Vec<_> = self
1515 .terminal_buffers
1516 .values()
1517 .copied()
1518 .filter_map(|terminal_id| {
1519 self.terminal_backing_files
1520 .get(&terminal_id)
1521 .map(|path| (terminal_id, path.clone()))
1522 })
1523 .collect();
1524
1525 for (terminal_id, backing_path) in terminals_to_sync {
1526 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1527 if let Ok(state) = handle.state.lock() {
1528 if let Ok(mut file) = self
1529 .resources
1530 .authority
1531 .filesystem
1532 .open_file_for_append(&backing_path)
1533 {
1534 let mut writer = BufWriter::new(&mut *file);
1535 if let Err(e) = state.append_visible_screen(&mut writer) {
1536 tracing::warn!(
1537 "Failed to sync terminal {:?} to backing file: {}",
1538 terminal_id,
1539 e
1540 );
1541 }
1542 }
1543 }
1544 }
1545 }
1546 }
1547
1548 pub(crate) fn create_unnamed_recovery_buffer(
1552 &mut self,
1553 text: &str,
1554 recovery_id: String,
1555 display_name: String,
1556 ) -> BufferId {
1557 let buffer_id = self.alloc_buffer_id();
1558 let mut state = EditorState::new(
1559 self.terminal_width,
1560 self.terminal_height,
1561 self.resources.config.editor.large_file_threshold_bytes as usize,
1562 std::sync::Arc::clone(&self.resources.authority.filesystem),
1563 );
1564 state
1565 .margins
1566 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1567 state.buffer.set_default_line_ending(
1568 self.resources
1569 .config
1570 .editor
1571 .default_line_ending
1572 .to_line_ending(),
1573 );
1574 state.buffer.insert(0, text);
1575 state.buffer.set_modified(true);
1576 state.buffer.set_recovery_pending(false);
1577 self.buffers.insert(buffer_id, state);
1578
1579 let mut log = crate::model::event::EventLog::new();
1580 log.clear_saved_position();
1581 self.event_logs.insert(buffer_id, log);
1582
1583 let mut meta = crate::app::types::BufferMetadata::new();
1584 meta.recovery_id = Some(recovery_id);
1585 meta.display_name = display_name;
1586 self.buffer_metadata.insert(buffer_id, meta);
1587
1588 buffer_id
1589 }
1590
1591 pub(crate) fn seed_initial_layout(&mut self) {
1595 if self.buffers.splits().is_some() && self.buffers.len() > 0 {
1596 return;
1597 }
1598 let buf = self.alloc_buffer_id();
1599 let mut state = EditorState::new(
1600 self.terminal_width,
1601 self.terminal_height,
1602 self.resources.config.editor.large_file_threshold_bytes as usize,
1603 std::sync::Arc::clone(&self.resources.authority.filesystem),
1604 );
1605 state
1606 .margins
1607 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1608 state.buffer.set_default_line_ending(
1609 self.resources
1610 .config
1611 .editor
1612 .default_line_ending
1613 .to_line_ending(),
1614 );
1615 let manager = crate::view::split::SplitManager::new(buf);
1616 let active_leaf = manager.active_split();
1617 let mut view_states = HashMap::new();
1618 view_states.insert(
1619 active_leaf,
1620 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buf),
1621 );
1622 self.buffers.set_splits((manager, view_states));
1623 self.buffers.insert(buf, state);
1624 self.buffer_metadata
1625 .insert(buf, crate::app::types::BufferMetadata::new());
1626 self.event_logs
1627 .insert(buf, crate::model::event::EventLog::new());
1628 }
1629
1630 pub(crate) fn sync_lsp_after_recovery_replay(&mut self, buffer_id: BufferId) {
1634 let Some(text) = self
1635 .buffers
1636 .get(&buffer_id)
1637 .and_then(|state| state.buffer.to_string())
1638 else {
1639 return;
1640 };
1641 let full_change = lsp_types::TextDocumentContentChangeEvent {
1642 range: None,
1643 range_length: None,
1644 text,
1645 };
1646 self.send_lsp_changes_for_buffer(buffer_id, vec![full_change]);
1647 }
1648
1649 fn restore_unnamed_buffers(
1655 &mut self,
1656 unnamed_buffers: &[UnnamedBufferRef],
1657 ) -> HashMap<String, BufferId> {
1658 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1659 if !self.resources.config.editor.hot_exit || unnamed_buffers.is_empty() {
1660 return unnamed_buffer_map;
1661 }
1662 tracing::debug!(
1663 "Restoring {} unnamed buffers from recovery",
1664 unnamed_buffers.len()
1665 );
1666 for unnamed_ref in unnamed_buffers {
1667 let entries = match self
1668 .resources
1669 .recovery_service
1670 .lock()
1671 .unwrap()
1672 .list_recoverable()
1673 {
1674 Ok(e) => e,
1675 Err(e) => {
1676 tracing::warn!("Failed to list recovery entries: {}", e);
1677 continue;
1678 }
1679 };
1680 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1681 tracing::debug!(
1682 "Recovery file not found for unnamed buffer {}",
1683 unnamed_ref.recovery_id
1684 );
1685 continue;
1686 };
1687 let loaded = self
1688 .resources
1689 .recovery_service
1690 .lock()
1691 .unwrap()
1692 .load_recovery(entry);
1693 match loaded {
1694 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1695 let text = String::from_utf8_lossy(&content).into_owned();
1696 let buffer_id = self.create_unnamed_recovery_buffer(
1697 &text,
1698 unnamed_ref.recovery_id.clone(),
1699 unnamed_ref.display_name.clone(),
1700 );
1701 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1702 tracing::info!(
1703 "Restored unnamed buffer '{}' (recovery_id={})",
1704 unnamed_ref.display_name,
1705 unnamed_ref.recovery_id
1706 );
1707 }
1708 Ok(other) => {
1709 tracing::warn!(
1710 "Unexpected recovery result for unnamed buffer {}: {:?}",
1711 unnamed_ref.recovery_id,
1712 std::mem::discriminant(&other)
1713 );
1714 }
1715 Err(e) => {
1716 tracing::warn!(
1717 "Failed to load recovery for unnamed buffer {}: {}",
1718 unnamed_ref.recovery_id,
1719 e
1720 );
1721 }
1722 }
1723 }
1724 unnamed_buffer_map
1725 }
1726
1727 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1731 if !self.resources.config.editor.hot_exit {
1732 return;
1733 }
1734 let entries = self
1735 .resources
1736 .recovery_service
1737 .lock()
1738 .unwrap()
1739 .list_recoverable()
1740 .unwrap_or_default();
1741 if entries.is_empty() {
1742 return;
1743 }
1744 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1745 for buffer_id in buffer_ids {
1746 let file_path = self
1747 .buffers
1748 .get(&buffer_id)
1749 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1750 let Some(file_path) = file_path else { continue };
1751
1752 let recovery_id = self
1753 .resources
1754 .recovery_service
1755 .lock()
1756 .unwrap()
1757 .get_buffer_id(Some(&file_path));
1758 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1759 continue;
1760 };
1761 let loaded = self
1762 .resources
1763 .recovery_service
1764 .lock()
1765 .unwrap()
1766 .load_recovery(entry);
1767 match loaded {
1768 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1769 let mut mutated = false;
1770 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1771 let current_len = state.buffer.total_bytes();
1772 let text = String::from_utf8_lossy(&content).into_owned();
1773 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1774 let current_text = current
1775 .as_ref()
1776 .map(|b| String::from_utf8_lossy(b).into_owned());
1777 if current_text.as_deref() != Some(&text) {
1778 state.buffer.delete(0..current_len);
1779 state.buffer.insert(0, &text);
1780 state.buffer.set_modified(true);
1781 state.buffer.set_recovery_pending(false);
1782 mutated = true;
1783 tracing::info!(
1784 "Restored unsaved changes for {:?} from hot exit recovery",
1785 file_path
1786 );
1787 }
1788 }
1789 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1790 log.clear_saved_position();
1791 }
1792 if mutated {
1793 self.sync_lsp_after_recovery_replay(buffer_id);
1794 }
1795 }
1796 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
1797 chunks, ..
1798 }) => {
1799 let mut mutated = false;
1800 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1801 for chunk in chunks.into_iter().rev() {
1802 let text = String::from_utf8_lossy(&chunk.content).into_owned();
1803 if chunk.original_len > 0 {
1804 state
1805 .buffer
1806 .delete(chunk.offset..chunk.offset + chunk.original_len);
1807 }
1808 state.buffer.insert(chunk.offset, &text);
1809 }
1810 state.buffer.set_modified(true);
1811 state.buffer.set_recovery_pending(false);
1812 mutated = true;
1813 tracing::info!(
1814 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
1815 file_path
1816 );
1817 }
1818 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1819 log.clear_saved_position();
1820 }
1821 if mutated {
1822 self.sync_lsp_after_recovery_replay(buffer_id);
1823 }
1824 }
1825 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1826 original_path,
1827 ..
1828 }) => {
1829 let name = original_path
1830 .file_name()
1831 .unwrap_or_default()
1832 .to_string_lossy();
1833 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1834 self.set_status_message(format!(
1835 "{} changed on disk; unsaved changes not restored",
1836 name
1837 ));
1838 }
1839 Ok(_) => {} Err(e) => {
1841 tracing::debug!(
1842 "Failed to load hot exit recovery for {:?}: {}",
1843 file_path,
1844 e
1845 );
1846 }
1847 }
1848 }
1849 }
1850
1851 pub(crate) fn apply_workspace_layout(
1867 &mut self,
1868 workspace: &Workspace,
1869 session_name: Option<&str>,
1870 ) {
1871 tracing::debug!(
1872 "Applying workspace layout with {} split states",
1873 workspace.split_states.len()
1874 );
1875
1876 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
1879 self.mouse_enabled = mouse_enabled;
1880 }
1881
1882 self.restore_search_options(&workspace.search_options);
1883 self.restore_prompt_histories(&workspace.histories);
1884 self.restore_file_explorer_settings(&workspace.file_explorer);
1885
1886 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
1889
1890 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
1891 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
1892 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
1893
1894 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
1895
1896 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
1897 self.restore_split_node(
1898 &workspace.split_layout,
1899 &path_to_buffer,
1900 &terminal_buffer_map,
1901 &unnamed_buffer_map,
1902 &workspace.split_states,
1903 &mut split_id_map,
1904 true,
1905 );
1906
1907 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
1908 self.buffers
1909 .split_manager_mut()
1910 .expect("window must have a populated split layout")
1911 .set_active_split(LeafId(new_active_split));
1912 }
1913
1914 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
1915 self.clean_orphaned_buffers();
1916 self.log_restore_summary(session_name);
1917
1918 self.restore_hot_exit_changes(&path_to_buffer);
1920 }
1921
1922 pub(crate) fn from_workspace(
1928 id: fresh_core::WindowId,
1929 label: impl Into<String>,
1930 root: PathBuf,
1931 resources: crate::app::window_resources::WindowResources,
1932 workspace: &Workspace,
1933 ) -> Self {
1934 let mut window = Self::new(id, label, root, resources);
1935 window.seed_initial_layout();
1936 window.apply_workspace_layout(workspace, None);
1937 window
1938 }
1939
1940 pub(crate) fn capture_workspace(&self) -> Workspace {
1946 tracing::debug!("Capturing workspace for {:?}", self.root);
1947
1948 let mut terminals = Vec::new();
1949 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
1950 let mut seen = HashSet::new();
1951 for terminal_id in self.terminal_buffers.values().copied() {
1952 if seen.insert(terminal_id) {
1953 if self.ephemeral_terminals.contains(&terminal_id) {
1954 continue;
1955 }
1956 let idx = terminals.len();
1957 terminal_indices.insert(terminal_id, idx);
1958 let handle = self.terminal_manager.get(terminal_id);
1959 let (cols, rows) = handle
1960 .map(|h| h.size())
1961 .unwrap_or((self.terminal_width, self.terminal_height));
1962 let cwd = handle.and_then(|h| h.cwd());
1963 let shell = handle
1964 .map(|h| h.shell().to_string())
1965 .unwrap_or_else(crate::services::terminal::detect_shell);
1966 let log_path = self
1967 .terminal_log_files
1968 .get(&terminal_id)
1969 .cloned()
1970 .unwrap_or_else(|| {
1971 let root = self.resources.dir_context.terminal_dir_for(&self.root);
1972 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
1973 });
1974 let backing_path = self
1975 .terminal_backing_files
1976 .get(&terminal_id)
1977 .cloned()
1978 .unwrap_or_else(|| {
1979 let root = self.resources.dir_context.terminal_dir_for(&self.root);
1980 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
1981 });
1982
1983 terminals.push(SerializedTerminalWorkspace {
1984 terminal_index: idx,
1985 cwd,
1986 shell,
1987 cols,
1988 rows,
1989 log_path,
1990 backing_path,
1991 });
1992 }
1993 }
1994
1995 let (mgr, view_states) = self
1996 .buffers
1997 .splits()
1998 .expect("window must have a populated split layout");
1999
2000 let split_layout = serialize_split_node(
2001 mgr.root(),
2002 &self.buffer_metadata,
2003 &self.root,
2004 &self.terminal_buffers,
2005 &terminal_indices,
2006 mgr.labels(),
2007 );
2008
2009 let active_buffers: HashMap<LeafId, BufferId> = mgr
2010 .root()
2011 .get_leaves_with_rects(ratatui::layout::Rect::default())
2012 .into_iter()
2013 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
2014 .collect();
2015
2016 let mut split_states = HashMap::new();
2017 for (leaf_id, view_state) in view_states {
2018 let active_buffer = active_buffers.get(leaf_id).copied();
2019 let serialized = serialize_split_view_state(
2020 view_state,
2021 self.buffers.as_map(),
2022 &self.buffer_metadata,
2023 &self.root,
2024 active_buffer,
2025 &self.terminal_buffers,
2026 &terminal_indices,
2027 );
2028 split_states.insert(leaf_id.0 .0, serialized);
2029 }
2030
2031 let file_explorer = if let Some(explorer) = self.file_explorer.as_ref() {
2032 let expanded_dirs = get_expanded_dirs(explorer, &self.root);
2033 FileExplorerState {
2034 visible: self.file_explorer_visible,
2035 width: self.file_explorer_width,
2036 side: self.file_explorer_side,
2037 expanded_dirs,
2038 scroll_offset: explorer.get_scroll_offset(),
2039 show_hidden: explorer.ignore_patterns().show_hidden(),
2040 show_gitignored: explorer.ignore_patterns().show_gitignored(),
2041 }
2042 } else {
2043 FileExplorerState {
2044 visible: self.file_explorer_visible,
2045 width: self.file_explorer_width,
2046 side: self.file_explorer_side,
2047 expanded_dirs: Vec::new(),
2048 scroll_offset: 0,
2049 show_hidden: false,
2050 show_gitignored: false,
2051 }
2052 };
2053
2054 let cfg = &self.resources.config.editor;
2055 let config_overrides = WorkspaceConfigOverrides {
2056 line_numbers: Some(cfg.line_numbers),
2057 relative_line_numbers: Some(cfg.relative_line_numbers),
2058 line_wrap: Some(cfg.line_wrap),
2059 syntax_highlighting: Some(cfg.syntax_highlighting),
2060 enable_inlay_hints: Some(cfg.enable_inlay_hints),
2061 mouse_enabled: Some(self.mouse_enabled),
2062 menu_bar_hidden: None,
2063 };
2064
2065 let histories = WorkspaceHistories {
2066 search: self
2067 .prompt_histories
2068 .get("search")
2069 .map(|h| h.items().to_vec())
2070 .unwrap_or_default(),
2071 replace: self
2072 .prompt_histories
2073 .get("replace")
2074 .map(|h| h.items().to_vec())
2075 .unwrap_or_default(),
2076 command_palette: Vec::new(),
2077 goto_line: self
2078 .prompt_histories
2079 .get("goto_line")
2080 .map(|h| h.items().to_vec())
2081 .unwrap_or_default(),
2082 open_file: Vec::new(),
2083 };
2084
2085 let search_options = SearchOptions {
2086 case_sensitive: self.search_case_sensitive,
2087 whole_word: self.search_whole_word,
2088 use_regex: self.search_use_regex,
2089 confirm_each: self.search_confirm_each,
2090 };
2091
2092 let bookmarks = serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.root);
2093
2094 let external_files: Vec<PathBuf> = self
2095 .buffer_metadata
2096 .values()
2097 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2098 .filter_map(|meta| meta.file_path())
2099 .filter(|abs_path| abs_path.strip_prefix(&self.root).is_err())
2100 .cloned()
2101 .collect();
2102
2103 let read_only_files: Vec<PathBuf> = self
2104 .buffer_metadata
2105 .values()
2106 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2107 .filter(|meta| meta.read_only)
2108 .filter_map(|meta| meta.file_path().cloned())
2109 .filter(|p| !p.as_os_str().is_empty())
2110 .map(|p| {
2111 p.strip_prefix(&self.root)
2112 .map(|rel| rel.to_path_buf())
2113 .unwrap_or(p)
2114 })
2115 .collect();
2116
2117 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.resources.config.editor.hot_exit {
2118 self.buffer_metadata
2119 .iter()
2120 .filter_map(|(buffer_id, meta)| {
2121 let path = meta.file_path()?;
2122 if !path.as_os_str().is_empty() {
2123 return None;
2124 }
2125 if meta.hidden_from_tabs || meta.is_virtual() {
2126 return None;
2127 }
2128 let state = self.buffers.get(buffer_id)?;
2129 if state.buffer.total_bytes() == 0 {
2130 return None;
2131 }
2132 let recovery_id = meta.recovery_id.clone()?;
2133 Some(UnnamedBufferRef {
2134 recovery_id,
2135 display_name: meta.display_name.clone(),
2136 })
2137 })
2138 .collect()
2139 } else {
2140 Vec::new()
2141 };
2142
2143 Workspace {
2144 version: WORKSPACE_VERSION,
2145 working_dir: self.root.clone(),
2146 split_layout,
2147 active_split_id: SplitId::from(mgr.active_split()).0,
2148 split_states,
2149 config_overrides,
2150 file_explorer,
2151 histories,
2152 search_options,
2153 bookmarks,
2154 terminals,
2155 external_files,
2156 read_only_files,
2157 unnamed_buffers,
2158 plugin_global_state: HashMap::new(),
2159 saved_at: std::time::SystemTime::now()
2160 .duration_since(std::time::UNIX_EPOCH)
2161 .unwrap_or_default()
2162 .as_secs(),
2163 }
2164 }
2165}
2166
2167fn get_first_leaf_buffer(
2169 node: &SerializedSplitNode,
2170 path_to_buffer: &HashMap<PathBuf, BufferId>,
2171 terminal_buffers: &HashMap<usize, BufferId>,
2172 unnamed_buffers: &HashMap<String, BufferId>,
2173) -> Option<BufferId> {
2174 match node {
2175 SerializedSplitNode::Leaf {
2176 file_path,
2177 unnamed_recovery_id,
2178 ..
2179 } => file_path
2180 .as_ref()
2181 .and_then(|p| path_to_buffer.get(p).copied())
2182 .or_else(|| {
2183 unnamed_recovery_id
2184 .as_ref()
2185 .and_then(|id| unnamed_buffers.get(id).copied())
2186 }),
2187 SerializedSplitNode::Terminal { terminal_index, .. } => {
2188 terminal_buffers.get(terminal_index).copied()
2189 }
2190 SerializedSplitNode::Split { first, .. } => {
2191 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2192 }
2193 }
2194}
2195
2196fn serialize_split_node(
2201 node: &SplitNode,
2202 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2203 working_dir: &Path,
2204 terminal_buffers: &HashMap<BufferId, TerminalId>,
2205 terminal_indices: &HashMap<TerminalId, usize>,
2206 split_labels: &HashMap<SplitId, String>,
2207) -> SerializedSplitNode {
2208 serialize_split_node_pruned(
2209 node,
2210 buffer_metadata,
2211 working_dir,
2212 terminal_buffers,
2213 terminal_indices,
2214 split_labels,
2215 )
2216 .unwrap_or({
2217 SerializedSplitNode::Leaf {
2220 file_path: None,
2221 split_id: 0,
2222 label: None,
2223 unnamed_recovery_id: None,
2224 role: None,
2225 }
2226 })
2227}
2228
2229fn serialize_split_node_pruned(
2236 node: &SplitNode,
2237 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2238 working_dir: &Path,
2239 terminal_buffers: &HashMap<BufferId, TerminalId>,
2240 terminal_indices: &HashMap<TerminalId, usize>,
2241 split_labels: &HashMap<SplitId, String>,
2242) -> Option<SerializedSplitNode> {
2243 match node {
2244 SplitNode::Grouped { layout, .. } => {
2245 serialize_split_node_pruned(
2249 layout,
2250 buffer_metadata,
2251 working_dir,
2252 terminal_buffers,
2253 terminal_indices,
2254 split_labels,
2255 )
2256 }
2257 SplitNode::Leaf {
2258 buffer_id,
2259 split_id,
2260 role,
2261 } => {
2262 let raw_split_id: SplitId = (*split_id).into();
2263 let label = split_labels.get(&raw_split_id).cloned();
2264 let role = *role;
2265
2266 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2267 if let Some(index) = terminal_indices.get(terminal_id) {
2268 return Some(SerializedSplitNode::Terminal {
2269 terminal_index: *index,
2270 split_id: raw_split_id.0,
2271 label,
2272 role,
2273 });
2274 }
2275 }
2276
2277 let meta = buffer_metadata.get(buffer_id);
2278
2279 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2283 return None;
2284 }
2285
2286 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2287 if abs_path.as_os_str().is_empty() {
2288 None } else {
2290 abs_path
2291 .strip_prefix(working_dir)
2292 .ok()
2293 .map(|p| p.to_path_buf())
2294 }
2295 });
2296
2297 let unnamed_recovery_id = if file_path.is_none() {
2300 meta.and_then(|m| m.recovery_id.clone())
2301 } else {
2302 None
2303 };
2304
2305 Some(SerializedSplitNode::Leaf {
2306 file_path,
2307 split_id: raw_split_id.0,
2308 label,
2309 unnamed_recovery_id,
2310 role,
2311 })
2312 }
2313 SplitNode::Split {
2314 direction,
2315 first,
2316 second,
2317 ratio,
2318 split_id,
2319 ..
2320 } => {
2321 let raw_split_id: SplitId = (*split_id).into();
2322 let first = serialize_split_node_pruned(
2323 first,
2324 buffer_metadata,
2325 working_dir,
2326 terminal_buffers,
2327 terminal_indices,
2328 split_labels,
2329 );
2330 let second = serialize_split_node_pruned(
2331 second,
2332 buffer_metadata,
2333 working_dir,
2334 terminal_buffers,
2335 terminal_indices,
2336 split_labels,
2337 );
2338 match (first, second) {
2339 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2340 direction: match direction {
2341 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2342 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2343 },
2344 first: Box::new(f),
2345 second: Box::new(s),
2346 ratio: *ratio,
2347 split_id: raw_split_id.0,
2348 }),
2349 (Some(only), None) | (None, Some(only)) => Some(only),
2352 (None, None) => None,
2353 }
2354 }
2355 }
2356}
2357
2358fn serialize_split_view_state(
2359 view_state: &crate::view::split::SplitViewState,
2360 buffers: &HashMap<BufferId, EditorState>,
2361 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2362 working_dir: &Path,
2363 active_buffer: Option<BufferId>,
2364 terminal_buffers: &HashMap<BufferId, TerminalId>,
2365 terminal_indices: &HashMap<TerminalId, usize>,
2366) -> SerializedSplitViewState {
2367 let mut open_tabs = Vec::new();
2368 let mut open_files = Vec::new();
2369 let mut active_tab_index = None;
2370
2371 for buffer_id in view_state.buffer_tab_ids() {
2373 let buffer_id = &buffer_id;
2374 let tab_index = open_tabs.len();
2375 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2376 if let Some(idx) = terminal_indices.get(terminal_id) {
2377 open_tabs.push(SerializedTabRef::Terminal(*idx));
2378 if Some(*buffer_id) == active_buffer {
2379 active_tab_index = Some(tab_index);
2380 }
2381 continue;
2382 }
2383 }
2384
2385 if let Some(meta) = buffer_metadata.get(buffer_id) {
2386 if let Some(abs_path) = meta.file_path() {
2387 if abs_path.as_os_str().is_empty() {
2388 if let Some(ref recovery_id) = meta.recovery_id {
2390 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2391 if Some(*buffer_id) == active_buffer {
2392 active_tab_index = Some(tab_index);
2393 }
2394 }
2395 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2396 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2397 open_files.push(rel_path.to_path_buf());
2398 if Some(*buffer_id) == active_buffer {
2399 active_tab_index = Some(tab_index);
2400 }
2401 } else {
2402 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2404 if Some(*buffer_id) == active_buffer {
2405 active_tab_index = Some(tab_index);
2406 }
2407 }
2408 }
2409 }
2410 }
2411
2412 let active_file_index = active_tab_index
2414 .and_then(|idx| open_tabs.get(idx))
2415 .and_then(|tab| match tab {
2416 SerializedTabRef::File(path) => {
2417 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2418 }
2419 _ => None,
2420 })
2421 .unwrap_or(0);
2422
2423 let mut file_states = HashMap::new();
2425 for (buffer_id, buf_state) in &view_state.keyed_states {
2426 let Some(meta) = buffer_metadata.get(buffer_id) else {
2427 continue;
2428 };
2429 let Some(abs_path) = meta.file_path() else {
2430 continue;
2431 };
2432
2433 let state_key = if abs_path.as_os_str().is_empty() {
2435 if let Some(ref recovery_id) = meta.recovery_id {
2437 PathBuf::from(format!("__unnamed__{}", recovery_id))
2438 } else {
2439 continue;
2440 }
2441 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2442 rp.to_path_buf()
2443 } else {
2444 abs_path.to_path_buf()
2446 };
2447
2448 let primary_cursor = buf_state.cursors.primary();
2449 let folds = buffers
2450 .get(buffer_id)
2451 .map(|state| {
2452 buf_state
2453 .folds
2454 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2455 .into_iter()
2456 .map(|range| SerializedFoldRange {
2457 header_line: range.header_line,
2458 end_line: range.end_line,
2459 placeholder: range.placeholder,
2460 header_text: range.header_text,
2461 })
2462 .collect::<Vec<_>>()
2463 })
2464 .unwrap_or_default();
2465
2466 file_states.insert(
2467 state_key,
2468 SerializedFileState {
2469 cursor: SerializedCursor {
2470 position: primary_cursor.position,
2471 anchor: primary_cursor.anchor,
2472 sticky_column: primary_cursor.sticky_column,
2473 },
2474 additional_cursors: buf_state
2475 .cursors
2476 .iter()
2477 .skip(1) .map(|(_, cursor)| SerializedCursor {
2479 position: cursor.position,
2480 anchor: cursor.anchor,
2481 sticky_column: cursor.sticky_column,
2482 })
2483 .collect(),
2484 scroll: SerializedScroll {
2485 top_byte: buf_state.viewport.top_byte,
2486 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2487 left_column: buf_state.viewport.left_column,
2488 },
2489 view_mode: match buf_state.view_mode {
2490 ViewMode::Source => SerializedViewMode::Source,
2491 ViewMode::PageView => SerializedViewMode::PageView,
2492 },
2493 compose_width: buf_state.compose_width,
2494 plugin_state: buf_state.plugin_state.clone(),
2495 folds,
2496 },
2497 );
2498 }
2499
2500 let active_view_mode = active_buffer
2502 .and_then(|id| view_state.keyed_states.get(&id))
2503 .map(|bs| match bs.view_mode {
2504 ViewMode::Source => SerializedViewMode::Source,
2505 ViewMode::PageView => SerializedViewMode::PageView,
2506 })
2507 .unwrap_or(SerializedViewMode::Source);
2508 let active_compose_width = active_buffer
2509 .and_then(|id| view_state.keyed_states.get(&id))
2510 .and_then(|bs| bs.compose_width);
2511
2512 SerializedSplitViewState {
2513 open_tabs,
2514 active_tab_index,
2515 open_files,
2516 active_file_index,
2517 file_states,
2518 tab_scroll_offset: view_state.tab_scroll_offset,
2519 view_mode: active_view_mode,
2520 compose_width: active_compose_width,
2521 }
2522}
2523
2524fn serialize_bookmarks(
2525 bookmarks: &BookmarkState,
2526 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2527 working_dir: &Path,
2528) -> HashMap<char, SerializedBookmark> {
2529 bookmarks
2530 .iter()
2531 .filter_map(|(key, bookmark)| {
2532 buffer_metadata
2533 .get(&bookmark.buffer_id)
2534 .and_then(|meta| meta.file_path())
2535 .and_then(|abs_path| {
2536 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2537 (
2538 key,
2539 SerializedBookmark {
2540 file_path: rel_path.to_path_buf(),
2541 position: bookmark.position,
2542 },
2543 )
2544 })
2545 })
2546 })
2547 .collect()
2548}
2549
2550fn collect_file_paths_from_states(
2552 split_states: &HashMap<usize, SerializedSplitViewState>,
2553) -> Vec<PathBuf> {
2554 let mut paths = Vec::new();
2555 for state in split_states.values() {
2556 if !state.open_tabs.is_empty() {
2557 for tab in &state.open_tabs {
2558 if let SerializedTabRef::File(path) = tab {
2559 if !paths.contains(path) {
2560 paths.push(path.clone());
2561 }
2562 }
2563 }
2564 } else {
2565 for path in &state.open_files {
2566 if !paths.contains(path) {
2567 paths.push(path.clone());
2568 }
2569 }
2570 }
2571 }
2572 paths
2573}
2574
2575fn get_expanded_dirs(
2577 explorer: &crate::view::file_tree::FileTreeView,
2578 working_dir: &Path,
2579) -> Vec<PathBuf> {
2580 let mut expanded = Vec::new();
2581 let tree = explorer.tree();
2582
2583 for node in tree.all_nodes() {
2585 if node.is_expanded() && node.is_dir() {
2586 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2588 expanded.push(rel_path.to_path_buf());
2589 }
2590 }
2591 }
2592
2593 expanded
2594}