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 self.resources.config.editor.scroll_offset,
1011 );
1012 self.buffers
1013 .split_view_states_mut()
1014 .expect("active window must have a populated split layout")
1015 .insert(new_leaf_id, view_state);
1016
1017 split_id_map.insert(*split_id, new_leaf_id.into());
1019
1020 self.restore_split_node(
1022 second,
1023 path_to_buffer,
1024 terminal_buffers,
1025 unnamed_buffers,
1026 split_states,
1027 split_id_map,
1028 false,
1029 );
1030 }
1031 Err(e) => {
1032 tracing::error!("Failed to create split during workspace restore: {}", e);
1033 }
1034 }
1035 }
1036 }
1037 }
1038
1039 fn restore_split_view_state(
1041 &mut self,
1042 current_split_id: LeafId,
1043 saved_split_id: usize,
1044 split_states: &HashMap<usize, SerializedSplitViewState>,
1045 path_to_buffer: &HashMap<PathBuf, BufferId>,
1046 terminal_buffers: &HashMap<usize, BufferId>,
1047 unnamed_buffers: &HashMap<String, BufferId>,
1048 ) {
1049 let Some(split_state) = split_states.get(&saved_split_id) else {
1051 return;
1052 };
1053
1054 let split_buf_for_current = self
1058 .buffers
1059 .split_manager()
1060 .expect("active window must have a populated split layout")
1061 .buffer_for_split(current_split_id);
1062 let active_buffer_id = self
1063 .buffers
1064 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1065 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1066 return None;
1067 };
1068 let mut active_buffer_id: Option<BufferId> = None;
1069 if !split_state.open_tabs.is_empty() {
1070 view_state.open_buffers.clear();
1073
1074 for tab in &split_state.open_tabs {
1075 match tab {
1076 SerializedTabRef::File(rel_path) => {
1077 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1078 if !view_state.has_buffer(buffer_id) {
1079 view_state.add_buffer(buffer_id);
1080 }
1081 view_state.ensure_buffer_state(buffer_id);
1083 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1084 let buf_state =
1085 view_state.buffer_state_mut(buffer_id).unwrap();
1086 buf_state.viewport.line_wrap_enabled = false;
1087 buf_state.show_line_numbers = false;
1091 buf_state.highlight_current_line = false;
1092 }
1093 }
1094 }
1095 SerializedTabRef::Terminal(index) => {
1096 if let Some(&buffer_id) = terminal_buffers.get(index) {
1097 if !view_state.has_buffer(buffer_id) {
1098 view_state.add_buffer(buffer_id);
1099 }
1100 let buf_state = view_state.ensure_buffer_state(buffer_id);
1101 buf_state.viewport.line_wrap_enabled = false;
1102 buf_state.show_line_numbers = false;
1106 buf_state.highlight_current_line = false;
1107 }
1108 }
1109 SerializedTabRef::Unnamed(recovery_id) => {
1110 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1111 if !view_state.has_buffer(buffer_id) {
1112 view_state.add_buffer(buffer_id);
1113 }
1114 view_state.ensure_buffer_state(buffer_id);
1115 }
1116 }
1117 }
1118 }
1119
1120 if view_state.open_buffers.is_empty() {
1125 if let Some(buf) = split_buf_for_current {
1126 view_state.add_buffer(buf);
1127 view_state.ensure_buffer_state(buf);
1128 }
1129 }
1130
1131 if let Some(active_idx) = split_state.active_tab_index {
1132 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1133 active_buffer_id = match tab {
1134 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1135 SerializedTabRef::Terminal(index) => {
1136 terminal_buffers.get(index).copied()
1137 }
1138 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1139 };
1140 }
1141 }
1142 } else {
1143 for rel_path in &split_state.open_files {
1145 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1146 if !view_state.has_buffer(buffer_id) {
1147 view_state.add_buffer(buffer_id);
1148 }
1149 view_state.ensure_buffer_state(buffer_id);
1150 }
1151 }
1152
1153 let active_file_path =
1154 split_state.open_files.get(split_state.active_file_index);
1155 active_buffer_id =
1156 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1157 }
1158
1159 for (rel_path, file_state) in &split_state.file_states {
1161 let rel_str = rel_path.to_string_lossy();
1163 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1164 match unnamed_buffers.get(recovery_id).copied() {
1165 Some(id) => id,
1166 None => continue,
1167 }
1168 } else {
1169 match path_to_buffer.get(rel_path).copied() {
1170 Some(id) => id,
1171 None => continue,
1172 }
1173 };
1174 let max_pos = __buffers_mut
1175 .get(&buffer_id)
1176 .map(|b| b.buffer.len())
1177 .unwrap_or(0);
1178
1179 let buf_state = view_state.ensure_buffer_state(buffer_id);
1181
1182 let cursor_pos = file_state.cursor.position.min(max_pos);
1183 buf_state.cursors.primary_mut().position = cursor_pos;
1184 buf_state.cursors.primary_mut().anchor =
1185 file_state.cursor.anchor.map(|a| a.min(max_pos));
1186 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1187
1188 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1189 buf_state.viewport.top_view_line_offset =
1190 file_state.scroll.top_view_line_offset;
1191 buf_state.viewport.left_column = file_state.scroll.left_column;
1192 buf_state.viewport.set_skip_resize_sync();
1193
1194 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1202 super::navigation::reconcile_restored_buffer_view(
1203 buf_state,
1204 &mut state.buffer,
1205 );
1206 }
1207
1208 buf_state.view_mode = match file_state.view_mode {
1210 SerializedViewMode::Source => ViewMode::Source,
1211 SerializedViewMode::PageView => ViewMode::PageView,
1212 };
1213 buf_state.compose_width = file_state.compose_width;
1214 buf_state.plugin_state = file_state.plugin_state.clone();
1215 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1216 buf_state.folds.clear(&mut state.marker_list);
1217 for fold in &file_state.folds {
1218 let Some(resolved_header) = resolve_fold_header_line(
1225 &state.buffer,
1226 fold.header_line,
1227 fold.header_text.as_deref(),
1228 ) else {
1229 tracing::debug!(
1230 "Dropping stale fold: header_line={} no longer matches stored \
1231 header_text after external edit",
1232 fold.header_line,
1233 );
1234 continue;
1235 };
1236
1237 let shift = resolved_header as i64 - fold.header_line as i64;
1239 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1240 let start_line = resolved_header.saturating_add(1);
1241 let end_line = adjusted_end;
1242 if start_line > end_line {
1243 continue;
1244 }
1245 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1246 else {
1247 continue;
1248 };
1249 let end_byte = state
1250 .buffer
1251 .line_start_offset(end_line.saturating_add(1))
1252 .unwrap_or_else(|| state.buffer.len());
1253 buf_state.folds.add(
1254 &mut state.marker_list,
1255 start_byte,
1256 end_byte,
1257 fold.placeholder.clone(),
1258 );
1259 }
1260 }
1261
1262 tracing::trace!(
1263 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1264 rel_path,
1265 cursor_pos,
1266 buf_state.viewport.top_byte,
1267 buf_state.view_mode,
1268 );
1269 }
1270
1271 let restored_view_mode = match split_state.view_mode {
1274 SerializedViewMode::Source => ViewMode::Source,
1275 SerializedViewMode::PageView => ViewMode::PageView,
1276 };
1277
1278 if let Some(active_buf_id) = active_buffer_id {
1279 view_state.switch_buffer(active_buf_id);
1281
1282 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
1284 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
1285 });
1286 if !active_has_file_state {
1287 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1288 view_state.active_state_mut().compose_width = split_state.compose_width;
1289 }
1290
1291 }
1293 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1294 active_buffer_id
1295 })
1296 .flatten();
1297
1298 if let Some(active_buf_id) = active_buffer_id {
1302 self.buffers
1303 .split_manager_mut()
1304 .expect("active window must have a populated split layout")
1305 .set_split_buffer(current_split_id, active_buf_id);
1306 }
1307 }
1308
1309 fn restore_search_options(&mut self, opts: &SearchOptions) {
1310 self.search_case_sensitive = opts.case_sensitive;
1311 self.search_whole_word = opts.whole_word;
1312 self.search_use_regex = opts.use_regex;
1313 self.search_confirm_each = opts.confirm_each;
1314 }
1315
1316 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
1317 tracing::debug!(
1318 "Restoring histories: {} search, {} replace, {} goto_line",
1319 histories.search.len(),
1320 histories.replace.len(),
1321 histories.goto_line.len()
1322 );
1323 for item in &histories.search {
1324 self.prompt_histories
1325 .entry("search".to_string())
1326 .or_default()
1327 .push(item.clone());
1328 }
1329 for item in &histories.replace {
1330 self.prompt_histories
1331 .entry("replace".to_string())
1332 .or_default()
1333 .push(item.clone());
1334 }
1335 for item in &histories.goto_line {
1336 self.prompt_histories
1337 .entry("goto_line".to_string())
1338 .or_default()
1339 .push(item.clone());
1340 }
1341 }
1342
1343 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
1344 self.file_explorer_visible = fe.visible;
1345 self.file_explorer_width = fe.width;
1346 self.file_explorer_side = fe.side;
1347
1348 if fe.show_hidden {
1350 self.pending_file_explorer_show_hidden = Some(true);
1351 }
1352 if fe.show_gitignored {
1353 self.pending_file_explorer_show_gitignored = Some(true);
1354 }
1355
1356 if self.file_explorer_visible && self.file_explorer.is_none() {
1358 self.init_file_explorer();
1359 }
1360 }
1361
1362 fn open_workspace_files(
1365 &mut self,
1366 split_states: &HashMap<usize, SerializedSplitViewState>,
1367 ) -> HashMap<PathBuf, BufferId> {
1368 let file_paths = collect_file_paths_from_states(split_states);
1369 tracing::debug!(
1370 "Workspace has {} files to restore: {:?}",
1371 file_paths.len(),
1372 file_paths
1373 );
1374 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
1375 for rel_path in file_paths {
1376 let abs_path = self.root.join(&rel_path);
1377 tracing::trace!(
1378 "Checking file: {:?} (exists: {})",
1379 abs_path,
1380 abs_path.exists()
1381 );
1382 if abs_path.exists() {
1383 match self.open_file_internal(&abs_path) {
1384 Ok(buffer_id) => {
1385 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
1386 path_to_buffer.insert(rel_path, buffer_id);
1387 }
1388 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
1389 }
1390 } else {
1391 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
1392 }
1393 }
1394 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
1395 path_to_buffer
1396 }
1397
1398 fn restore_external_files(
1400 &mut self,
1401 external_files: &[PathBuf],
1402 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
1403 ) {
1404 if external_files.is_empty() {
1405 return;
1406 }
1407 tracing::debug!(
1408 "Restoring {} external files: {:?}",
1409 external_files.len(),
1410 external_files
1411 );
1412 for abs_path in external_files {
1413 if !abs_path.exists() {
1414 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
1415 continue;
1416 }
1417 match self.open_file_internal(abs_path) {
1418 Ok(buffer_id) => {
1419 path_to_buffer.insert(abs_path.clone(), buffer_id);
1420 tracing::debug!(
1421 "Restored external file {:?} as buffer {:?}",
1422 abs_path,
1423 buffer_id
1424 );
1425 }
1426 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1427 }
1428 }
1429 }
1430
1431 fn apply_read_only_flags(
1434 &mut self,
1435 read_only_files: &[PathBuf],
1436 path_to_buffer: &HashMap<PathBuf, BufferId>,
1437 ) {
1438 for ro_path in read_only_files {
1439 let buffer_id = path_to_buffer
1440 .get(ro_path)
1441 .copied()
1442 .or_else(|| path_to_buffer.get(&self.root.join(ro_path)).copied());
1443 if let Some(id) = buffer_id {
1444 self.mark_buffer_read_only(id, true);
1445 }
1446 }
1447 }
1448
1449 pub(crate) fn has_any_virtual_buffer(&self) -> bool {
1454 self.buffer_metadata
1455 .values()
1456 .any(|m| matches!(m.kind, crate::app::types::BufferKind::Virtual { .. }))
1457 }
1458
1459 pub(crate) fn save_all_global_file_states(&self) {
1462 for (leaf_id, view_state) in self
1463 .buffers
1464 .splits()
1465 .map(|(_, vs)| vs)
1466 .expect("window must have a populated split layout")
1467 {
1468 let active_buffer = self
1469 .buffers
1470 .splits()
1471 .map(|(mgr, _)| mgr)
1472 .expect("window must have a populated split layout")
1473 .root()
1474 .get_leaves_with_rects(ratatui::layout::Rect::default())
1475 .into_iter()
1476 .find(|(sid, _, _)| *sid == *leaf_id)
1477 .map(|(_, buffer_id, _)| buffer_id);
1478
1479 if let Some(buffer_id) = active_buffer {
1480 self.save_buffer_file_state(buffer_id, view_state);
1481 }
1482 }
1483 }
1484
1485 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
1487 let abs_path = match self.buffer_metadata.get(&buffer_id) {
1488 Some(metadata) => match metadata.file_path() {
1489 Some(path) => path.to_path_buf(),
1490 None => return,
1491 },
1492 None => return,
1493 };
1494
1495 let primary_cursor = view_state.cursors.primary();
1496 let file_state = SerializedFileState {
1497 cursor: SerializedCursor {
1498 position: primary_cursor.position,
1499 anchor: primary_cursor.anchor,
1500 sticky_column: primary_cursor.sticky_column,
1501 },
1502 additional_cursors: view_state
1503 .cursors
1504 .iter()
1505 .skip(1)
1506 .map(|(_, cursor)| SerializedCursor {
1507 position: cursor.position,
1508 anchor: cursor.anchor,
1509 sticky_column: cursor.sticky_column,
1510 })
1511 .collect(),
1512 scroll: SerializedScroll {
1513 top_byte: view_state.viewport.top_byte,
1514 top_view_line_offset: view_state.viewport.top_view_line_offset,
1515 left_column: view_state.viewport.left_column,
1516 },
1517 view_mode: Default::default(),
1518 compose_width: None,
1519 plugin_state: std::collections::HashMap::new(),
1520 folds: Vec::new(),
1521 };
1522
1523 PersistedFileWorkspace::save(&abs_path, file_state);
1524 }
1525
1526 pub(crate) fn sync_terminal_backing_files(&self) {
1529 use std::io::BufWriter;
1530
1531 let terminals_to_sync: Vec<_> = self
1532 .terminal_buffers
1533 .values()
1534 .copied()
1535 .filter_map(|terminal_id| {
1536 self.terminal_backing_files
1537 .get(&terminal_id)
1538 .map(|path| (terminal_id, path.clone()))
1539 })
1540 .collect();
1541
1542 for (terminal_id, backing_path) in terminals_to_sync {
1543 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1544 if let Ok(mut state) = handle.state.lock() {
1545 if let Ok(mut file) = self
1550 .resources
1551 .authority
1552 .filesystem
1553 .open_file_for_append(&backing_path)
1554 {
1555 let mut writer = BufWriter::new(&mut *file);
1556 if let Err(e) = state.flush_new_scrollback(&mut writer) {
1557 tracing::warn!(
1558 "Failed to flush terminal {:?} scrollback: {}",
1559 terminal_id,
1560 e
1561 );
1562 }
1563 }
1564
1565 if let Ok(mut file) = self
1566 .resources
1567 .authority
1568 .filesystem
1569 .open_file_for_append(&backing_path)
1570 {
1571 let mut writer = BufWriter::new(&mut *file);
1572 if let Err(e) = state.append_visible_screen(&mut writer) {
1573 tracing::warn!(
1574 "Failed to sync terminal {:?} to backing file: {}",
1575 terminal_id,
1576 e
1577 );
1578 }
1579 }
1580 }
1581 }
1582 }
1583 }
1584
1585 pub(crate) fn create_unnamed_recovery_buffer(
1589 &mut self,
1590 text: &str,
1591 recovery_id: String,
1592 display_name: String,
1593 ) -> BufferId {
1594 let buffer_id = self.alloc_buffer_id();
1595 let mut state = EditorState::new(
1596 self.terminal_width,
1597 self.terminal_height,
1598 self.resources.config.editor.large_file_threshold_bytes as usize,
1599 std::sync::Arc::clone(&self.resources.authority.filesystem),
1600 );
1601 state
1602 .margins
1603 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1604 state.buffer.set_default_line_ending(
1605 self.resources
1606 .config
1607 .editor
1608 .default_line_ending
1609 .to_line_ending(),
1610 );
1611 state.buffer.insert(0, text);
1612 state.buffer.set_modified(true);
1613 state.buffer.set_recovery_pending(false);
1614 self.buffers.insert(buffer_id, state);
1615
1616 let mut log = crate::model::event::EventLog::new();
1617 log.clear_saved_position();
1618 self.event_logs.insert(buffer_id, log);
1619
1620 let mut meta = crate::app::types::BufferMetadata::new();
1621 meta.recovery_id = Some(recovery_id);
1622 meta.display_name = display_name;
1623 self.buffer_metadata.insert(buffer_id, meta);
1624
1625 buffer_id
1626 }
1627
1628 pub(crate) fn seed_initial_layout(&mut self) {
1632 if self.buffers.splits().is_some() && self.buffers.len() > 0 {
1633 return;
1634 }
1635 let buf = self.alloc_buffer_id();
1636 let mut state = EditorState::new(
1637 self.terminal_width,
1638 self.terminal_height,
1639 self.resources.config.editor.large_file_threshold_bytes as usize,
1640 std::sync::Arc::clone(&self.resources.authority.filesystem),
1641 );
1642 state
1643 .margins
1644 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1645 state.buffer.set_default_line_ending(
1646 self.resources
1647 .config
1648 .editor
1649 .default_line_ending
1650 .to_line_ending(),
1651 );
1652 let manager = crate::view::split::SplitManager::new(buf);
1653 let active_leaf = manager.active_split();
1654 let mut view_states = HashMap::new();
1655 view_states.insert(
1656 active_leaf,
1657 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buf),
1658 );
1659 self.buffers.set_splits((manager, view_states));
1660 self.buffers.insert(buf, state);
1661 self.buffer_metadata
1662 .insert(buf, crate::app::types::BufferMetadata::new());
1663 self.event_logs
1664 .insert(buf, crate::model::event::EventLog::new());
1665 }
1666
1667 pub(crate) fn sync_lsp_after_recovery_replay(&mut self, buffer_id: BufferId) {
1671 let Some(text) = self
1672 .buffers
1673 .get(&buffer_id)
1674 .and_then(|state| state.buffer.to_string())
1675 else {
1676 return;
1677 };
1678 let full_change = lsp_types::TextDocumentContentChangeEvent {
1679 range: None,
1680 range_length: None,
1681 text,
1682 };
1683 self.send_lsp_changes_for_buffer(buffer_id, vec![full_change]);
1684 }
1685
1686 fn restore_unnamed_buffers(
1692 &mut self,
1693 unnamed_buffers: &[UnnamedBufferRef],
1694 ) -> HashMap<String, BufferId> {
1695 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1696 if !self.resources.config.editor.hot_exit || unnamed_buffers.is_empty() {
1697 return unnamed_buffer_map;
1698 }
1699 tracing::debug!(
1700 "Restoring {} unnamed buffers from recovery",
1701 unnamed_buffers.len()
1702 );
1703 for unnamed_ref in unnamed_buffers {
1704 let entries = match self
1705 .resources
1706 .recovery_service
1707 .lock()
1708 .unwrap()
1709 .list_recoverable()
1710 {
1711 Ok(e) => e,
1712 Err(e) => {
1713 tracing::warn!("Failed to list recovery entries: {}", e);
1714 continue;
1715 }
1716 };
1717 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1718 tracing::debug!(
1719 "Recovery file not found for unnamed buffer {}",
1720 unnamed_ref.recovery_id
1721 );
1722 continue;
1723 };
1724 let loaded = self
1725 .resources
1726 .recovery_service
1727 .lock()
1728 .unwrap()
1729 .load_recovery(entry);
1730 match loaded {
1731 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1732 let text = String::from_utf8_lossy(&content).into_owned();
1733 let buffer_id = self.create_unnamed_recovery_buffer(
1734 &text,
1735 unnamed_ref.recovery_id.clone(),
1736 unnamed_ref.display_name.clone(),
1737 );
1738 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1739 tracing::info!(
1740 "Restored unnamed buffer '{}' (recovery_id={})",
1741 unnamed_ref.display_name,
1742 unnamed_ref.recovery_id
1743 );
1744 }
1745 Ok(other) => {
1746 tracing::warn!(
1747 "Unexpected recovery result for unnamed buffer {}: {:?}",
1748 unnamed_ref.recovery_id,
1749 std::mem::discriminant(&other)
1750 );
1751 }
1752 Err(e) => {
1753 tracing::warn!(
1754 "Failed to load recovery for unnamed buffer {}: {}",
1755 unnamed_ref.recovery_id,
1756 e
1757 );
1758 }
1759 }
1760 }
1761 unnamed_buffer_map
1762 }
1763
1764 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1768 if !self.resources.config.editor.hot_exit {
1769 return;
1770 }
1771 let entries = self
1772 .resources
1773 .recovery_service
1774 .lock()
1775 .unwrap()
1776 .list_recoverable()
1777 .unwrap_or_default();
1778 if entries.is_empty() {
1779 return;
1780 }
1781 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1782 for buffer_id in buffer_ids {
1783 let file_path = self
1784 .buffers
1785 .get(&buffer_id)
1786 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1787 let Some(file_path) = file_path else { continue };
1788
1789 let recovery_id = self
1790 .resources
1791 .recovery_service
1792 .lock()
1793 .unwrap()
1794 .get_buffer_id(Some(&file_path));
1795 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1796 continue;
1797 };
1798 let loaded = self
1799 .resources
1800 .recovery_service
1801 .lock()
1802 .unwrap()
1803 .load_recovery(entry);
1804 match loaded {
1805 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1806 let mut mutated = false;
1807 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1808 let current_len = state.buffer.total_bytes();
1809 let text = String::from_utf8_lossy(&content).into_owned();
1810 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1811 let current_text = current
1812 .as_ref()
1813 .map(|b| String::from_utf8_lossy(b).into_owned());
1814 if current_text.as_deref() != Some(&text) {
1815 state.buffer.delete(0..current_len);
1816 state.buffer.insert(0, &text);
1817 state.buffer.set_modified(true);
1818 state.buffer.set_recovery_pending(false);
1819 mutated = true;
1820 tracing::info!(
1821 "Restored unsaved changes for {:?} from hot exit recovery",
1822 file_path
1823 );
1824 }
1825 }
1826 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1827 log.clear_saved_position();
1828 }
1829 if mutated {
1830 self.sync_lsp_after_recovery_replay(buffer_id);
1831 }
1832 }
1833 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
1834 chunks, ..
1835 }) => {
1836 let mut mutated = false;
1837 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1838 for chunk in chunks.into_iter().rev() {
1839 let text = String::from_utf8_lossy(&chunk.content).into_owned();
1840 if chunk.original_len > 0 {
1841 state
1842 .buffer
1843 .delete(chunk.offset..chunk.offset + chunk.original_len);
1844 }
1845 state.buffer.insert(chunk.offset, &text);
1846 }
1847 state.buffer.set_modified(true);
1848 state.buffer.set_recovery_pending(false);
1849 mutated = true;
1850 tracing::info!(
1851 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
1852 file_path
1853 );
1854 }
1855 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1856 log.clear_saved_position();
1857 }
1858 if mutated {
1859 self.sync_lsp_after_recovery_replay(buffer_id);
1860 }
1861 }
1862 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1863 original_path,
1864 ..
1865 }) => {
1866 let name = original_path
1867 .file_name()
1868 .unwrap_or_default()
1869 .to_string_lossy();
1870 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1871 self.set_status_message(format!(
1872 "{} changed on disk; unsaved changes not restored",
1873 name
1874 ));
1875 }
1876 Ok(_) => {} Err(e) => {
1878 tracing::debug!(
1879 "Failed to load hot exit recovery for {:?}: {}",
1880 file_path,
1881 e
1882 );
1883 }
1884 }
1885 }
1886 }
1887
1888 pub(crate) fn apply_workspace_layout(
1904 &mut self,
1905 workspace: &Workspace,
1906 session_name: Option<&str>,
1907 ) {
1908 tracing::debug!(
1909 "Applying workspace layout with {} split states",
1910 workspace.split_states.len()
1911 );
1912
1913 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
1916 self.mouse_enabled = mouse_enabled;
1917 }
1918
1919 self.restore_search_options(&workspace.search_options);
1920 self.restore_prompt_histories(&workspace.histories);
1921 self.restore_file_explorer_settings(&workspace.file_explorer);
1922
1923 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
1926
1927 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
1928 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
1929 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
1930
1931 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
1932
1933 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
1934 self.restore_split_node(
1935 &workspace.split_layout,
1936 &path_to_buffer,
1937 &terminal_buffer_map,
1938 &unnamed_buffer_map,
1939 &workspace.split_states,
1940 &mut split_id_map,
1941 true,
1942 );
1943
1944 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
1945 self.buffers
1946 .split_manager_mut()
1947 .expect("window must have a populated split layout")
1948 .set_active_split(LeafId(new_active_split));
1949 }
1950
1951 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
1952 self.clean_orphaned_buffers();
1953 self.log_restore_summary(session_name);
1954
1955 self.restore_hot_exit_changes(&path_to_buffer);
1957 }
1958
1959 pub(crate) fn from_workspace(
1965 id: fresh_core::WindowId,
1966 label: impl Into<String>,
1967 root: PathBuf,
1968 resources: crate::app::window_resources::WindowResources,
1969 workspace: &Workspace,
1970 ) -> Self {
1971 let mut window = Self::new(id, label, root, resources);
1972 window.seed_initial_layout();
1973 window.apply_workspace_layout(workspace, None);
1974 window
1975 }
1976
1977 pub(crate) fn capture_workspace(&self) -> Workspace {
1983 tracing::debug!("Capturing workspace for {:?}", self.root);
1984
1985 let mut terminals = Vec::new();
1986 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
1987 let mut seen = HashSet::new();
1988 for terminal_id in self.terminal_buffers.values().copied() {
1989 if seen.insert(terminal_id) {
1990 if self.ephemeral_terminals.contains(&terminal_id) {
1991 continue;
1992 }
1993 let idx = terminals.len();
1994 terminal_indices.insert(terminal_id, idx);
1995 let handle = self.terminal_manager.get(terminal_id);
1996 let (cols, rows) = handle
1997 .map(|h| h.size())
1998 .unwrap_or((self.terminal_width, self.terminal_height));
1999 let cwd = handle.and_then(|h| h.cwd());
2000 let shell = handle
2001 .map(|h| h.shell().to_string())
2002 .unwrap_or_else(crate::services::terminal::detect_shell);
2003 let log_path = self
2004 .terminal_log_files
2005 .get(&terminal_id)
2006 .cloned()
2007 .unwrap_or_else(|| {
2008 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2009 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
2010 });
2011 let backing_path = self
2012 .terminal_backing_files
2013 .get(&terminal_id)
2014 .cloned()
2015 .unwrap_or_else(|| {
2016 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2017 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
2018 });
2019
2020 terminals.push(SerializedTerminalWorkspace {
2021 terminal_index: idx,
2022 cwd,
2023 shell,
2024 cols,
2025 rows,
2026 log_path,
2027 backing_path,
2028 });
2029 }
2030 }
2031
2032 let (mgr, view_states) = self
2033 .buffers
2034 .splits()
2035 .expect("window must have a populated split layout");
2036
2037 let split_layout = serialize_split_node(
2038 mgr.root(),
2039 &self.buffer_metadata,
2040 &self.root,
2041 &self.terminal_buffers,
2042 &terminal_indices,
2043 mgr.labels(),
2044 );
2045
2046 let active_buffers: HashMap<LeafId, BufferId> = mgr
2047 .root()
2048 .get_leaves_with_rects(ratatui::layout::Rect::default())
2049 .into_iter()
2050 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
2051 .collect();
2052
2053 let mut split_states = HashMap::new();
2054 for (leaf_id, view_state) in view_states {
2055 let active_buffer = active_buffers.get(leaf_id).copied();
2056 let serialized = serialize_split_view_state(
2057 view_state,
2058 self.buffers.as_map(),
2059 &self.buffer_metadata,
2060 &self.root,
2061 active_buffer,
2062 &self.terminal_buffers,
2063 &terminal_indices,
2064 );
2065 split_states.insert(leaf_id.0 .0, serialized);
2066 }
2067
2068 let file_explorer = if let Some(explorer) = self.file_explorer.as_ref() {
2069 let expanded_dirs = get_expanded_dirs(explorer, &self.root);
2070 FileExplorerState {
2071 visible: self.file_explorer_visible,
2072 width: self.file_explorer_width,
2073 side: self.file_explorer_side,
2074 expanded_dirs,
2075 scroll_offset: explorer.get_scroll_offset(),
2076 show_hidden: explorer.ignore_patterns().show_hidden(),
2077 show_gitignored: explorer.ignore_patterns().show_gitignored(),
2078 }
2079 } else {
2080 FileExplorerState {
2081 visible: self.file_explorer_visible,
2082 width: self.file_explorer_width,
2083 side: self.file_explorer_side,
2084 expanded_dirs: Vec::new(),
2085 scroll_offset: 0,
2086 show_hidden: false,
2087 show_gitignored: false,
2088 }
2089 };
2090
2091 let cfg = &self.resources.config.editor;
2092 let config_overrides = WorkspaceConfigOverrides {
2093 line_numbers: Some(cfg.line_numbers),
2094 relative_line_numbers: Some(cfg.relative_line_numbers),
2095 line_wrap: Some(cfg.line_wrap),
2096 syntax_highlighting: Some(cfg.syntax_highlighting),
2097 enable_inlay_hints: Some(cfg.enable_inlay_hints),
2098 mouse_enabled: Some(self.mouse_enabled),
2099 menu_bar_hidden: None,
2100 };
2101
2102 let histories = WorkspaceHistories {
2103 search: self
2104 .prompt_histories
2105 .get("search")
2106 .map(|h| h.items().to_vec())
2107 .unwrap_or_default(),
2108 replace: self
2109 .prompt_histories
2110 .get("replace")
2111 .map(|h| h.items().to_vec())
2112 .unwrap_or_default(),
2113 command_palette: Vec::new(),
2114 goto_line: self
2115 .prompt_histories
2116 .get("goto_line")
2117 .map(|h| h.items().to_vec())
2118 .unwrap_or_default(),
2119 open_file: Vec::new(),
2120 };
2121
2122 let search_options = SearchOptions {
2123 case_sensitive: self.search_case_sensitive,
2124 whole_word: self.search_whole_word,
2125 use_regex: self.search_use_regex,
2126 confirm_each: self.search_confirm_each,
2127 };
2128
2129 let bookmarks = serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.root);
2130
2131 let external_files: Vec<PathBuf> = self
2132 .buffer_metadata
2133 .values()
2134 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2135 .filter_map(|meta| meta.file_path())
2136 .filter(|abs_path| abs_path.strip_prefix(&self.root).is_err())
2137 .cloned()
2138 .collect();
2139
2140 let read_only_files: Vec<PathBuf> = self
2141 .buffer_metadata
2142 .values()
2143 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2144 .filter(|meta| meta.read_only)
2145 .filter_map(|meta| meta.file_path().cloned())
2146 .filter(|p| !p.as_os_str().is_empty())
2147 .map(|p| {
2148 p.strip_prefix(&self.root)
2149 .map(|rel| rel.to_path_buf())
2150 .unwrap_or(p)
2151 })
2152 .collect();
2153
2154 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.resources.config.editor.hot_exit {
2155 self.buffer_metadata
2156 .iter()
2157 .filter_map(|(buffer_id, meta)| {
2158 let path = meta.file_path()?;
2159 if !path.as_os_str().is_empty() {
2160 return None;
2161 }
2162 if meta.hidden_from_tabs || meta.is_virtual() {
2163 return None;
2164 }
2165 let state = self.buffers.get(buffer_id)?;
2166 if state.buffer.total_bytes() == 0 {
2167 return None;
2168 }
2169 let recovery_id = meta.recovery_id.clone()?;
2170 Some(UnnamedBufferRef {
2171 recovery_id,
2172 display_name: meta.display_name.clone(),
2173 })
2174 })
2175 .collect()
2176 } else {
2177 Vec::new()
2178 };
2179
2180 Workspace {
2181 version: WORKSPACE_VERSION,
2182 working_dir: self.root.clone(),
2183 split_layout,
2184 active_split_id: SplitId::from(mgr.active_split()).0,
2185 split_states,
2186 config_overrides,
2187 file_explorer,
2188 histories,
2189 search_options,
2190 bookmarks,
2191 terminals,
2192 external_files,
2193 read_only_files,
2194 unnamed_buffers,
2195 plugin_global_state: HashMap::new(),
2196 saved_at: std::time::SystemTime::now()
2197 .duration_since(std::time::UNIX_EPOCH)
2198 .unwrap_or_default()
2199 .as_secs(),
2200 label: Some(self.label.clone()),
2203 session_plugin_state: self.plugin_state.clone(),
2204 }
2205 }
2206}
2207
2208fn get_first_leaf_buffer(
2210 node: &SerializedSplitNode,
2211 path_to_buffer: &HashMap<PathBuf, BufferId>,
2212 terminal_buffers: &HashMap<usize, BufferId>,
2213 unnamed_buffers: &HashMap<String, BufferId>,
2214) -> Option<BufferId> {
2215 match node {
2216 SerializedSplitNode::Leaf {
2217 file_path,
2218 unnamed_recovery_id,
2219 ..
2220 } => file_path
2221 .as_ref()
2222 .and_then(|p| path_to_buffer.get(p).copied())
2223 .or_else(|| {
2224 unnamed_recovery_id
2225 .as_ref()
2226 .and_then(|id| unnamed_buffers.get(id).copied())
2227 }),
2228 SerializedSplitNode::Terminal { terminal_index, .. } => {
2229 terminal_buffers.get(terminal_index).copied()
2230 }
2231 SerializedSplitNode::Split { first, .. } => {
2232 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2233 }
2234 }
2235}
2236
2237fn serialize_split_node(
2242 node: &SplitNode,
2243 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2244 working_dir: &Path,
2245 terminal_buffers: &HashMap<BufferId, TerminalId>,
2246 terminal_indices: &HashMap<TerminalId, usize>,
2247 split_labels: &HashMap<SplitId, String>,
2248) -> SerializedSplitNode {
2249 serialize_split_node_pruned(
2250 node,
2251 buffer_metadata,
2252 working_dir,
2253 terminal_buffers,
2254 terminal_indices,
2255 split_labels,
2256 )
2257 .unwrap_or({
2258 SerializedSplitNode::Leaf {
2261 file_path: None,
2262 split_id: 0,
2263 label: None,
2264 unnamed_recovery_id: None,
2265 role: None,
2266 }
2267 })
2268}
2269
2270fn serialize_split_node_pruned(
2277 node: &SplitNode,
2278 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2279 working_dir: &Path,
2280 terminal_buffers: &HashMap<BufferId, TerminalId>,
2281 terminal_indices: &HashMap<TerminalId, usize>,
2282 split_labels: &HashMap<SplitId, String>,
2283) -> Option<SerializedSplitNode> {
2284 match node {
2285 SplitNode::Grouped { layout, .. } => {
2286 serialize_split_node_pruned(
2290 layout,
2291 buffer_metadata,
2292 working_dir,
2293 terminal_buffers,
2294 terminal_indices,
2295 split_labels,
2296 )
2297 }
2298 SplitNode::Leaf {
2299 buffer_id,
2300 split_id,
2301 role,
2302 } => {
2303 let raw_split_id: SplitId = (*split_id).into();
2304 let label = split_labels.get(&raw_split_id).cloned();
2305 let role = *role;
2306
2307 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2308 if let Some(index) = terminal_indices.get(terminal_id) {
2309 return Some(SerializedSplitNode::Terminal {
2310 terminal_index: *index,
2311 split_id: raw_split_id.0,
2312 label,
2313 role,
2314 });
2315 }
2316 }
2317
2318 let meta = buffer_metadata.get(buffer_id);
2319
2320 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2324 return None;
2325 }
2326
2327 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2328 if abs_path.as_os_str().is_empty() {
2329 None } else {
2331 abs_path
2332 .strip_prefix(working_dir)
2333 .ok()
2334 .map(|p| p.to_path_buf())
2335 }
2336 });
2337
2338 let unnamed_recovery_id = if file_path.is_none() {
2341 meta.and_then(|m| m.recovery_id.clone())
2342 } else {
2343 None
2344 };
2345
2346 Some(SerializedSplitNode::Leaf {
2347 file_path,
2348 split_id: raw_split_id.0,
2349 label,
2350 unnamed_recovery_id,
2351 role,
2352 })
2353 }
2354 SplitNode::Split {
2355 direction,
2356 first,
2357 second,
2358 ratio,
2359 split_id,
2360 ..
2361 } => {
2362 let raw_split_id: SplitId = (*split_id).into();
2363 let first = serialize_split_node_pruned(
2364 first,
2365 buffer_metadata,
2366 working_dir,
2367 terminal_buffers,
2368 terminal_indices,
2369 split_labels,
2370 );
2371 let second = serialize_split_node_pruned(
2372 second,
2373 buffer_metadata,
2374 working_dir,
2375 terminal_buffers,
2376 terminal_indices,
2377 split_labels,
2378 );
2379 match (first, second) {
2380 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2381 direction: match direction {
2382 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2383 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2384 },
2385 first: Box::new(f),
2386 second: Box::new(s),
2387 ratio: *ratio,
2388 split_id: raw_split_id.0,
2389 }),
2390 (Some(only), None) | (None, Some(only)) => Some(only),
2393 (None, None) => None,
2394 }
2395 }
2396 }
2397}
2398
2399fn serialize_split_view_state(
2400 view_state: &crate::view::split::SplitViewState,
2401 buffers: &HashMap<BufferId, EditorState>,
2402 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2403 working_dir: &Path,
2404 active_buffer: Option<BufferId>,
2405 terminal_buffers: &HashMap<BufferId, TerminalId>,
2406 terminal_indices: &HashMap<TerminalId, usize>,
2407) -> SerializedSplitViewState {
2408 let mut open_tabs = Vec::new();
2409 let mut open_files = Vec::new();
2410 let mut active_tab_index = None;
2411
2412 for buffer_id in view_state.buffer_tab_ids() {
2414 let buffer_id = &buffer_id;
2415 let tab_index = open_tabs.len();
2416 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2417 if let Some(idx) = terminal_indices.get(terminal_id) {
2418 open_tabs.push(SerializedTabRef::Terminal(*idx));
2419 if Some(*buffer_id) == active_buffer {
2420 active_tab_index = Some(tab_index);
2421 }
2422 continue;
2423 }
2424 }
2425
2426 if let Some(meta) = buffer_metadata.get(buffer_id) {
2427 if let Some(abs_path) = meta.file_path() {
2428 if abs_path.as_os_str().is_empty() {
2429 if let Some(ref recovery_id) = meta.recovery_id {
2431 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2432 if Some(*buffer_id) == active_buffer {
2433 active_tab_index = Some(tab_index);
2434 }
2435 }
2436 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2437 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2438 open_files.push(rel_path.to_path_buf());
2439 if Some(*buffer_id) == active_buffer {
2440 active_tab_index = Some(tab_index);
2441 }
2442 } else {
2443 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2445 if Some(*buffer_id) == active_buffer {
2446 active_tab_index = Some(tab_index);
2447 }
2448 }
2449 }
2450 }
2451 }
2452
2453 let active_file_index = active_tab_index
2455 .and_then(|idx| open_tabs.get(idx))
2456 .and_then(|tab| match tab {
2457 SerializedTabRef::File(path) => {
2458 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2459 }
2460 _ => None,
2461 })
2462 .unwrap_or(0);
2463
2464 let mut file_states = HashMap::new();
2466 for (buffer_id, buf_state) in &view_state.keyed_states {
2467 let Some(meta) = buffer_metadata.get(buffer_id) else {
2468 continue;
2469 };
2470 let Some(abs_path) = meta.file_path() else {
2471 continue;
2472 };
2473
2474 let state_key = if abs_path.as_os_str().is_empty() {
2476 if let Some(ref recovery_id) = meta.recovery_id {
2478 PathBuf::from(format!("__unnamed__{}", recovery_id))
2479 } else {
2480 continue;
2481 }
2482 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2483 rp.to_path_buf()
2484 } else {
2485 abs_path.to_path_buf()
2487 };
2488
2489 let primary_cursor = buf_state.cursors.primary();
2490 let folds = buffers
2491 .get(buffer_id)
2492 .map(|state| {
2493 buf_state
2494 .folds
2495 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2496 .into_iter()
2497 .map(|range| SerializedFoldRange {
2498 header_line: range.header_line,
2499 end_line: range.end_line,
2500 placeholder: range.placeholder,
2501 header_text: range.header_text,
2502 })
2503 .collect::<Vec<_>>()
2504 })
2505 .unwrap_or_default();
2506
2507 file_states.insert(
2508 state_key,
2509 SerializedFileState {
2510 cursor: SerializedCursor {
2511 position: primary_cursor.position,
2512 anchor: primary_cursor.anchor,
2513 sticky_column: primary_cursor.sticky_column,
2514 },
2515 additional_cursors: buf_state
2516 .cursors
2517 .iter()
2518 .skip(1) .map(|(_, cursor)| SerializedCursor {
2520 position: cursor.position,
2521 anchor: cursor.anchor,
2522 sticky_column: cursor.sticky_column,
2523 })
2524 .collect(),
2525 scroll: SerializedScroll {
2526 top_byte: buf_state.viewport.top_byte,
2527 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2528 left_column: buf_state.viewport.left_column,
2529 },
2530 view_mode: match buf_state.view_mode {
2531 ViewMode::Source => SerializedViewMode::Source,
2532 ViewMode::PageView => SerializedViewMode::PageView,
2533 },
2534 compose_width: buf_state.compose_width,
2535 plugin_state: buf_state.plugin_state.clone(),
2536 folds,
2537 },
2538 );
2539 }
2540
2541 let active_view_mode = active_buffer
2543 .and_then(|id| view_state.keyed_states.get(&id))
2544 .map(|bs| match bs.view_mode {
2545 ViewMode::Source => SerializedViewMode::Source,
2546 ViewMode::PageView => SerializedViewMode::PageView,
2547 })
2548 .unwrap_or(SerializedViewMode::Source);
2549 let active_compose_width = active_buffer
2550 .and_then(|id| view_state.keyed_states.get(&id))
2551 .and_then(|bs| bs.compose_width);
2552
2553 SerializedSplitViewState {
2554 open_tabs,
2555 active_tab_index,
2556 open_files,
2557 active_file_index,
2558 file_states,
2559 tab_scroll_offset: view_state.tab_scroll_offset,
2560 view_mode: active_view_mode,
2561 compose_width: active_compose_width,
2562 }
2563}
2564
2565fn serialize_bookmarks(
2566 bookmarks: &BookmarkState,
2567 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2568 working_dir: &Path,
2569) -> HashMap<char, SerializedBookmark> {
2570 bookmarks
2571 .iter()
2572 .filter_map(|(key, bookmark)| {
2573 buffer_metadata
2574 .get(&bookmark.buffer_id)
2575 .and_then(|meta| meta.file_path())
2576 .and_then(|abs_path| {
2577 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2578 (
2579 key,
2580 SerializedBookmark {
2581 file_path: rel_path.to_path_buf(),
2582 position: bookmark.position,
2583 },
2584 )
2585 })
2586 })
2587 })
2588 .collect()
2589}
2590
2591fn collect_file_paths_from_states(
2593 split_states: &HashMap<usize, SerializedSplitViewState>,
2594) -> Vec<PathBuf> {
2595 let mut paths = Vec::new();
2596 for state in split_states.values() {
2597 if !state.open_tabs.is_empty() {
2598 for tab in &state.open_tabs {
2599 if let SerializedTabRef::File(path) = tab {
2600 if !paths.contains(path) {
2601 paths.push(path.clone());
2602 }
2603 }
2604 }
2605 } else {
2606 for path in &state.open_files {
2607 if !paths.contains(path) {
2608 paths.push(path.clone());
2609 }
2610 }
2611 }
2612 }
2613 paths
2614}
2615
2616fn get_expanded_dirs(
2618 explorer: &crate::view::file_tree::FileTreeView,
2619 working_dir: &Path,
2620) -> Vec<PathBuf> {
2621 let mut expanded = Vec::new();
2622 let tree = explorer.tree();
2623
2624 for node in tree.all_nodes() {
2626 if node.is_expanded() && node.is_dir() {
2627 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2629 expanded.push(rel_path.to_path_buf());
2630 }
2631 }
2632 }
2633
2634 expanded
2635}