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