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