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() {
512 let on_disk = if let Some(ref session_name) = self.session_name {
513 Workspace::load_session(session_name, &self.working_dir)
514 .ok()
515 .flatten()
516 } else {
517 Workspace::load(&self.working_dir).ok().flatten()
518 };
519 if let Some(existing) = on_disk {
520 if !existing.has_no_real_content() {
521 tracing::info!(
522 "Skipping workspace save: only virtual buffers are open, \
523 on-disk workspace already has real content"
524 );
525 return Ok(());
526 }
527 }
528 }
529
530 if let Some(ref session_name) = self.session_name {
532 workspace.save_session(session_name)
533 } else {
534 workspace.save()
535 }
536 }
537
538 fn has_any_virtual_buffer(&self) -> bool {
543 self.active_window()
544 .buffer_metadata
545 .values()
546 .any(|m| matches!(m.kind, crate::app::types::BufferKind::Virtual { .. }))
547 }
548
549 fn save_all_global_file_states(&self) {
551 for (leaf_id, view_state) in self
553 .windows
554 .get(&self.active_window)
555 .and_then(|w| w.buffers.splits())
556 .map(|(_, vs)| vs)
557 .expect("active window must have a populated split layout")
558 {
559 let active_buffer = self
561 .windows
562 .get(&self.active_window)
563 .and_then(|w| w.buffers.splits())
564 .map(|(mgr, _)| mgr)
565 .expect("active window must have a populated split layout")
566 .root()
567 .get_leaves_with_rects(ratatui::layout::Rect::default())
568 .into_iter()
569 .find(|(sid, _, _)| *sid == *leaf_id)
570 .map(|(_, buffer_id, _)| buffer_id);
571
572 if let Some(buffer_id) = active_buffer {
573 self.save_buffer_file_state(buffer_id, view_state);
574 }
575 }
576 }
577
578 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
580 let abs_path = match self.active_window().buffer_metadata.get(&buffer_id) {
582 Some(metadata) => match metadata.file_path() {
583 Some(path) => path.to_path_buf(),
584 None => return, },
586 None => return,
587 };
588
589 let primary_cursor = view_state.cursors.primary();
591 let file_state = SerializedFileState {
592 cursor: SerializedCursor {
593 position: primary_cursor.position,
594 anchor: primary_cursor.anchor,
595 sticky_column: primary_cursor.sticky_column,
596 },
597 additional_cursors: view_state
598 .cursors
599 .iter()
600 .skip(1)
601 .map(|(_, cursor)| SerializedCursor {
602 position: cursor.position,
603 anchor: cursor.anchor,
604 sticky_column: cursor.sticky_column,
605 })
606 .collect(),
607 scroll: SerializedScroll {
608 top_byte: view_state.viewport.top_byte,
609 top_view_line_offset: view_state.viewport.top_view_line_offset,
610 left_column: view_state.viewport.left_column,
611 },
612 view_mode: Default::default(),
613 compose_width: None,
614 plugin_state: std::collections::HashMap::new(),
615 folds: Vec::new(),
616 };
617
618 PersistedFileWorkspace::save(&abs_path, file_state);
620 }
621
622 fn sync_all_terminal_backing_files(&mut self) {
627 use std::io::BufWriter;
628
629 let terminals_to_sync: Vec<_> = self
631 .active_window()
632 .terminal_buffers
633 .values()
634 .copied()
635 .filter_map(|terminal_id| {
636 self.active_window()
637 .terminal_backing_files
638 .get(&terminal_id)
639 .map(|path| (terminal_id, path.clone()))
640 })
641 .collect();
642
643 for (terminal_id, backing_path) in terminals_to_sync {
644 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
645 if let Ok(state) = handle.state.lock() {
646 if let Ok(mut file) = self
648 .authority
649 .filesystem
650 .open_file_for_append(&backing_path)
651 {
652 let mut writer = BufWriter::new(&mut *file);
653 if let Err(e) = state.append_visible_screen(&mut writer) {
654 tracing::warn!(
655 "Failed to sync terminal {:?} to backing file: {}",
656 terminal_id,
657 e
658 );
659 }
660 }
661 }
662 }
663 }
664 }
665
666 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
670 tracing::debug!("Attempting to restore workspace for {:?}", self.working_dir);
671
672 let workspace = if let Some(ref session_name) = self.session_name {
674 Workspace::load_session(session_name, &self.working_dir)?
675 } else {
676 Workspace::load(&self.working_dir)?
677 };
678
679 match workspace {
680 Some(workspace) => {
681 tracing::info!("Found workspace, applying...");
682 self.apply_workspace(&workspace)?;
683 Ok(true)
684 }
685 None => {
686 tracing::debug!("No workspace found for {:?}", self.working_dir);
687 Ok(false)
688 }
689 }
690 }
691
692 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
698 if !self.config.editor.hot_exit {
699 return Ok(0);
700 }
701
702 let entries = self.recovery_service.list_recoverable()?;
703 if entries.is_empty() {
704 return Ok(0);
705 }
706
707 let buffer_files: Vec<_> = self
709 .buffers()
710 .iter()
711 .filter_map(|(buffer_id, state)| {
712 let path = state.buffer.file_path()?.to_path_buf();
713 if path.as_os_str().is_empty() {
714 return None; }
716 Some((*buffer_id, path))
717 })
718 .collect();
719
720 let mut recovered = 0;
721 for (buffer_id, file_path) in buffer_files {
722 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
723 let entry = entries.iter().find(|e| e.id == recovery_id);
724 if let Some(entry) = entry {
725 match self.recovery_service.load_recovery(entry) {
726 Ok(crate::services::recovery::RecoveryResult::Recovered {
727 content, ..
728 }) => {
729 let mut mutated = false;
730 if let Some(state) = self
731 .windows
732 .get_mut(&self.active_window)
733 .map(|w| &mut w.buffers)
734 .expect("active window present")
735 .get_mut(&buffer_id)
736 {
737 let current_len = state.buffer.total_bytes();
738 let text = String::from_utf8_lossy(&content).into_owned();
739 let current = state.buffer.get_text_range_mut(0, current_len).ok();
740 let current_text = current
741 .as_ref()
742 .map(|b| String::from_utf8_lossy(b).into_owned());
743 if current_text.as_deref() != Some(&text) {
744 state.buffer.delete(0..current_len);
745 state.buffer.insert(0, &text);
746 state.buffer.set_modified(true);
747 state.buffer.set_recovery_pending(false);
748 if let Some(log) =
751 self.active_window_mut().event_logs.get_mut(&buffer_id)
752 {
753 log.clear_saved_position();
754 }
755 mutated = true;
756 recovered += 1;
757 tracing::info!(
758 "Restored unsaved changes for {:?} from hot exit recovery",
759 file_path
760 );
761 }
762 }
763 if mutated {
764 self.sync_lsp_after_recovery_replay(buffer_id);
765 }
766 }
767 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
768 chunks,
769 ..
770 }) => {
771 let mut mutated = false;
772 if let Some(state) = self
773 .windows
774 .get_mut(&self.active_window)
775 .map(|w| &mut w.buffers)
776 .expect("active window present")
777 .get_mut(&buffer_id)
778 {
779 for chunk in chunks.into_iter().rev() {
780 let text = String::from_utf8_lossy(&chunk.content).into_owned();
781 if chunk.original_len > 0 {
782 state
783 .buffer
784 .delete(chunk.offset..chunk.offset + chunk.original_len);
785 }
786 state.buffer.insert(chunk.offset, &text);
787 }
788 state.buffer.set_modified(true);
789 state.buffer.set_recovery_pending(false);
790 if let Some(log) =
793 self.active_window_mut().event_logs.get_mut(&buffer_id)
794 {
795 log.clear_saved_position();
796 }
797 mutated = true;
798 recovered += 1;
799 tracing::info!(
800 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
801 file_path
802 );
803 }
804 if mutated {
805 self.sync_lsp_after_recovery_replay(buffer_id);
806 }
807 }
808 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
809 original_path,
810 ..
811 }) => {
812 let name = original_path
813 .file_name()
814 .unwrap_or_default()
815 .to_string_lossy();
816 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
817 self.set_status_message(format!(
818 "{} changed on disk; unsaved changes not restored",
819 name
820 ));
821 }
822 Ok(_) => {} Err(e) => {
824 tracing::debug!(
825 "Failed to load hot exit recovery for {:?}: {}",
826 file_path,
827 e
828 );
829 }
830 }
831 }
832 }
833
834 Ok(recovered)
835 }
836
837 pub fn apply_workspace(&mut self, workspace: &Workspace) -> Result<(), WorkspaceError> {
839 tracing::debug!(
840 "Applying workspace with {} split states",
841 workspace.split_states.len()
842 );
843
844 self.restore_config_overrides(&workspace.config_overrides);
845
846 if !workspace.plugin_global_state.is_empty() {
847 tracing::debug!(
848 "Restoring plugin global state for {} plugins",
849 workspace.plugin_global_state.len()
850 );
851 self.plugin_global_state = workspace.plugin_global_state.clone();
852 }
853
854 self.restore_search_options(&workspace.search_options);
855 self.restore_prompt_histories(&workspace.histories);
856 self.restore_file_explorer_settings(&workspace.file_explorer);
857
858 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
859 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
860 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
861 self.restore_hot_exit_changes(&path_to_buffer);
862
863 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
864 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
865
866 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
867 self.restore_split_node(
868 &workspace.split_layout,
869 &path_to_buffer,
870 &terminal_buffer_map,
871 &unnamed_buffer_map,
872 &workspace.split_states,
873 &mut split_id_map,
874 true,
875 );
876
877 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
878 self.windows
879 .get_mut(&self.active_window)
880 .and_then(|w| w.split_manager_mut())
881 .expect("active window must have a populated split layout")
882 .set_active_split(LeafId(new_active_split));
883 }
884
885 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
886 self.clean_orphaned_buffers();
887 self.log_restore_summary();
888
889 #[cfg(feature = "plugins")]
890 {
891 let buffer_id = self.active_buffer();
892 self.update_plugin_state_snapshot();
893 tracing::debug!(
894 "Firing buffer_activated for active buffer {:?} after workspace restore",
895 buffer_id
896 );
897 self.plugin_manager.read().unwrap().run_hook(
898 "buffer_activated",
899 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
900 );
901 }
902
903 Ok(())
904 }
905
906 fn restore_config_overrides(&mut self, overrides: &WorkspaceConfigOverrides) {
907 if let Some(line_numbers) = overrides.line_numbers {
908 self.config_mut().editor.line_numbers = line_numbers;
909 }
910 if let Some(relative_line_numbers) = overrides.relative_line_numbers {
911 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
912 }
913 if let Some(line_wrap) = overrides.line_wrap {
914 self.config_mut().editor.line_wrap = line_wrap;
915 }
916 if let Some(syntax_highlighting) = overrides.syntax_highlighting {
917 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
918 }
919 if let Some(enable_inlay_hints) = overrides.enable_inlay_hints {
920 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
921 }
922 if let Some(mouse_enabled) = overrides.mouse_enabled {
923 self.active_window_mut().mouse_enabled = mouse_enabled;
924 }
925 }
930
931 fn restore_search_options(&mut self, opts: &SearchOptions) {
932 self.active_window_mut().search_case_sensitive = opts.case_sensitive;
933 self.active_window_mut().search_whole_word = opts.whole_word;
934 self.active_window_mut().search_use_regex = opts.use_regex;
935 self.active_window_mut().search_confirm_each = opts.confirm_each;
936 }
937
938 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
939 tracing::debug!(
940 "Restoring histories: {} search, {} replace, {} goto_line",
941 histories.search.len(),
942 histories.replace.len(),
943 histories.goto_line.len()
944 );
945 for item in &histories.search {
946 self.get_or_create_prompt_history("search")
947 .push(item.clone());
948 }
949 for item in &histories.replace {
950 self.get_or_create_prompt_history("replace")
951 .push(item.clone());
952 }
953 for item in &histories.goto_line {
954 self.get_or_create_prompt_history("goto_line")
955 .push(item.clone());
956 }
957 }
958
959 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
960 self.active_window_mut().file_explorer_visible = fe.visible;
961 self.active_window_mut().file_explorer_width = fe.width;
962 self.active_window_mut().file_explorer_side = fe.side;
963
964 if fe.show_hidden {
966 self.active_window_mut().pending_file_explorer_show_hidden = Some(true);
967 }
968 if fe.show_gitignored {
969 self.active_window_mut()
970 .pending_file_explorer_show_gitignored = Some(true);
971 }
972
973 if self.file_explorer_visible() && self.file_explorer().is_none() {
975 self.init_file_explorer();
976 }
977 }
978
979 fn open_workspace_files(
982 &mut self,
983 split_states: &HashMap<usize, SerializedSplitViewState>,
984 ) -> HashMap<PathBuf, BufferId> {
985 let file_paths = collect_file_paths_from_states(split_states);
986 tracing::debug!(
987 "Workspace has {} files to restore: {:?}",
988 file_paths.len(),
989 file_paths
990 );
991 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
992 for rel_path in file_paths {
993 let abs_path = self.working_dir.join(&rel_path);
994 tracing::trace!(
995 "Checking file: {:?} (exists: {})",
996 abs_path,
997 abs_path.exists()
998 );
999 if abs_path.exists() {
1000 match self.open_file_internal(&abs_path) {
1001 Ok(buffer_id) => {
1002 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
1003 path_to_buffer.insert(rel_path, buffer_id);
1004 }
1005 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
1006 }
1007 } else {
1008 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
1009 }
1010 }
1011 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
1012 path_to_buffer
1013 }
1014
1015 fn restore_external_files(
1017 &mut self,
1018 external_files: &[PathBuf],
1019 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
1020 ) {
1021 if external_files.is_empty() {
1022 return;
1023 }
1024 tracing::debug!(
1025 "Restoring {} external files: {:?}",
1026 external_files.len(),
1027 external_files
1028 );
1029 for abs_path in external_files {
1030 if !abs_path.exists() {
1031 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
1032 continue;
1033 }
1034 match self.open_file_internal(abs_path) {
1035 Ok(buffer_id) => {
1036 path_to_buffer.insert(abs_path.clone(), buffer_id);
1037 tracing::debug!(
1038 "Restored external file {:?} as buffer {:?}",
1039 abs_path,
1040 buffer_id
1041 );
1042 }
1043 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1044 }
1045 }
1046 }
1047
1048 fn apply_read_only_flags(
1051 &mut self,
1052 read_only_files: &[PathBuf],
1053 path_to_buffer: &HashMap<PathBuf, BufferId>,
1054 ) {
1055 for ro_path in read_only_files {
1056 let buffer_id = path_to_buffer
1057 .get(ro_path)
1058 .copied()
1059 .or_else(|| path_to_buffer.get(&self.working_dir.join(ro_path)).copied());
1060 if let Some(id) = buffer_id {
1061 self.active_window_mut().mark_buffer_read_only(id, true);
1062 }
1063 }
1064 }
1065
1066 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1069 if !self.config.editor.hot_exit {
1070 return;
1071 }
1072 let entries = self.recovery_service.list_recoverable().unwrap_or_default();
1073 if entries.is_empty() {
1074 return;
1075 }
1076 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1077 for buffer_id in buffer_ids {
1078 let file_path = self
1079 .buffers()
1080 .get(&buffer_id)
1081 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1082 let Some(file_path) = file_path else { continue };
1083
1084 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
1085 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1086 continue;
1087 };
1088 match self.recovery_service.load_recovery(entry) {
1089 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1090 let mut mutated = false;
1091 if let Some(state) = self
1092 .windows
1093 .get_mut(&self.active_window)
1094 .map(|w| &mut w.buffers)
1095 .expect("active window present")
1096 .get_mut(&buffer_id)
1097 {
1098 let current_len = state.buffer.total_bytes();
1099 let text = String::from_utf8_lossy(&content).into_owned();
1100 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1101 let current_text = current
1102 .as_ref()
1103 .map(|b| String::from_utf8_lossy(b).into_owned());
1104 if current_text.as_deref() != Some(&text) {
1105 state.buffer.delete(0..current_len);
1106 state.buffer.insert(0, &text);
1107 state.buffer.set_modified(true);
1108 state.buffer.set_recovery_pending(false);
1109 mutated = true;
1110 tracing::info!(
1111 "Restored unsaved changes for {:?} from hot exit recovery",
1112 file_path
1113 );
1114 }
1115 }
1116 if let Some(log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1117 log.clear_saved_position();
1118 }
1119 if mutated {
1120 self.sync_lsp_after_recovery_replay(buffer_id);
1121 }
1122 }
1123 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
1124 chunks, ..
1125 }) => {
1126 let mut mutated = false;
1127 if let Some(state) = self
1128 .windows
1129 .get_mut(&self.active_window)
1130 .map(|w| &mut w.buffers)
1131 .expect("active window present")
1132 .get_mut(&buffer_id)
1133 {
1134 for chunk in chunks.into_iter().rev() {
1135 let text = String::from_utf8_lossy(&chunk.content).into_owned();
1136 if chunk.original_len > 0 {
1137 state
1138 .buffer
1139 .delete(chunk.offset..chunk.offset + chunk.original_len);
1140 }
1141 state.buffer.insert(chunk.offset, &text);
1142 }
1143 state.buffer.set_modified(true);
1144 state.buffer.set_recovery_pending(false);
1145 mutated = true;
1146 tracing::info!(
1147 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
1148 file_path
1149 );
1150 }
1151 if let Some(log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1152 log.clear_saved_position();
1153 }
1154 if mutated {
1155 self.sync_lsp_after_recovery_replay(buffer_id);
1156 }
1157 }
1158 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1159 original_path,
1160 ..
1161 }) => {
1162 let name = original_path
1163 .file_name()
1164 .unwrap_or_default()
1165 .to_string_lossy();
1166 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1167 self.set_status_message(format!(
1168 "{} changed on disk; unsaved changes not restored",
1169 name
1170 ));
1171 }
1172 Ok(_) => {} Err(e) => {
1174 tracing::debug!(
1175 "Failed to load hot exit recovery for {:?}: {}",
1176 file_path,
1177 e
1178 );
1179 }
1180 }
1181 }
1182 }
1183
1184 fn restore_unnamed_buffers(
1187 &mut self,
1188 unnamed_buffers: &[UnnamedBufferRef],
1189 ) -> HashMap<String, BufferId> {
1190 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1191 if !self.config.editor.hot_exit || unnamed_buffers.is_empty() {
1192 return unnamed_buffer_map;
1193 }
1194 tracing::debug!(
1195 "Restoring {} unnamed buffers from recovery",
1196 unnamed_buffers.len()
1197 );
1198 for unnamed_ref in unnamed_buffers {
1199 let entries = match self.recovery_service.list_recoverable() {
1200 Ok(e) => e,
1201 Err(e) => {
1202 tracing::warn!("Failed to list recovery entries: {}", e);
1203 continue;
1204 }
1205 };
1206 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1207 tracing::debug!(
1208 "Recovery file not found for unnamed buffer {}",
1209 unnamed_ref.recovery_id
1210 );
1211 continue;
1212 };
1213 match self.recovery_service.load_recovery(entry) {
1214 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1215 let text = String::from_utf8_lossy(&content).into_owned();
1216 let buffer_id = self.new_buffer();
1217 {
1218 let state = self.active_state_mut();
1219 state.buffer.insert(0, &text);
1220 state.buffer.set_modified(true);
1221 state.buffer.set_recovery_pending(false);
1222 }
1223 self.active_event_log_mut().clear_saved_position();
1224 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id)
1225 {
1226 meta.recovery_id = Some(unnamed_ref.recovery_id.clone());
1227 meta.display_name = unnamed_ref.display_name.clone();
1228 }
1229 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1230 tracing::info!(
1231 "Restored unnamed buffer '{}' (recovery_id={})",
1232 unnamed_ref.display_name,
1233 unnamed_ref.recovery_id
1234 );
1235 }
1236 Ok(other) => {
1237 tracing::warn!(
1238 "Unexpected recovery result for unnamed buffer {}: {:?}",
1239 unnamed_ref.recovery_id,
1240 std::mem::discriminant(&other)
1241 );
1242 }
1243 Err(e) => {
1244 tracing::warn!(
1245 "Failed to load recovery for unnamed buffer {}: {}",
1246 unnamed_ref.recovery_id,
1247 e
1248 );
1249 }
1250 }
1251 }
1252 unnamed_buffer_map
1253 }
1254
1255 fn restore_terminals_from_workspace(
1257 &mut self,
1258 terminals: &[SerializedTerminalWorkspace],
1259 ) -> HashMap<usize, BufferId> {
1260 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
1261 if terminals.is_empty() {
1262 return terminal_buffer_map;
1263 }
1264 let __window_bridge = self.active_window().bridge.clone();
1265 self.active_window_mut()
1266 .terminal_manager
1267 .set_async_bridge(__window_bridge);
1268 for terminal in terminals {
1269 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
1270 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
1271 }
1272 }
1273 terminal_buffer_map
1274 }
1275
1276 fn restore_bookmarks_from_workspace(
1278 &mut self,
1279 bookmarks: &HashMap<char, SerializedBookmark>,
1280 path_to_buffer: &HashMap<PathBuf, BufferId>,
1281 ) {
1282 for (key, bookmark) in bookmarks {
1283 let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) else {
1284 continue;
1285 };
1286 if let Some(buffer) = self
1287 .windows
1288 .get(&self.active_window)
1289 .map(|w| &w.buffers)
1290 .expect("active window present")
1291 .get(&buffer_id)
1292 {
1293 let pos = bookmark.position.min(buffer.buffer.len());
1294 self.active_window_mut().bookmarks.set(
1295 *key,
1296 Bookmark {
1297 buffer_id,
1298 position: pos,
1299 },
1300 );
1301 }
1302 }
1303 }
1304
1305 fn clean_orphaned_buffers(&mut self) {
1308 let referenced: HashSet<BufferId> = self
1309 .windows
1310 .get(&self.active_window)
1311 .and_then(|w| w.buffers.splits())
1312 .map(|(_, vs)| vs)
1313 .expect("active window must have a populated split layout")
1314 .values()
1315 .flat_map(|vs| vs.buffer_tab_ids())
1316 .collect();
1317 let orphans: Vec<BufferId> = self
1318 .windows
1319 .get(&self.active_window)
1320 .map(|w| &w.buffers)
1321 .expect("active window present")
1322 .iter()
1323 .filter(|(id, state)| {
1324 !referenced.contains(id)
1325 && state.buffer.file_path().is_none()
1326 && !state.buffer.is_modified()
1327 })
1328 .map(|(id, _)| *id)
1329 .collect();
1330 for id in orphans {
1331 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
1332 self.windows
1333 .get_mut(&self.active_window)
1334 .map(|w| &mut w.buffers)
1335 .expect("active window present")
1336 .remove(&id);
1337 self.detach_buffer_from_all_windows(id);
1338 self.active_window_mut().event_logs.remove(&id);
1339 self.active_window_mut().buffer_metadata.remove(&id);
1340 }
1341 }
1342
1343 fn log_restore_summary(&mut self) {
1346 tracing::debug!(
1347 "Workspace restore complete: {} splits, {} buffers",
1348 self.windows
1349 .get(&self.active_window)
1350 .and_then(|w| w.buffers.splits())
1351 .map(|(_, vs)| vs)
1352 .expect("active window must have a populated split layout")
1353 .len(),
1354 self.windows
1355 .get(&self.active_window)
1356 .map(|w| &w.buffers)
1357 .expect("active window present")
1358 .len()
1359 );
1360 let restored_count = self.buffers().count_where(|id, _| {
1361 self.active_window()
1362 .buffer_metadata
1363 .get(&id)
1364 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
1365 });
1366 if restored_count == 0 {
1367 return;
1368 }
1369 let msg = match self
1370 .session_name
1371 .as_ref()
1372 .map(|n| format!("session '{}'", n))
1373 {
1374 Some(label) => format!("Restored {} ({} buffer(s))", label, restored_count),
1375 None => format!(
1376 "Restored {} buffer(s) from previous session",
1377 restored_count
1378 ),
1379 };
1380 self.set_status_message(msg);
1381 }
1382
1383 fn restore_terminal_from_workspace(
1392 &mut self,
1393 terminal: &SerializedTerminalWorkspace,
1394 ) -> Option<BufferId> {
1395 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
1397 let log_path = if terminal.log_path.is_absolute() {
1398 terminal.log_path.clone()
1399 } else {
1400 terminals_root.join(&terminal.log_path)
1401 };
1402 let backing_path = if terminal.backing_path.is_absolute() {
1403 terminal.backing_path.clone()
1404 } else {
1405 terminals_root.join(&terminal.backing_path)
1406 };
1407
1408 #[allow(clippy::let_underscore_must_use)]
1410 let _ = self.authority.filesystem.create_dir_all(
1411 log_path
1412 .parent()
1413 .or_else(|| backing_path.parent())
1414 .unwrap_or(&terminals_root),
1415 );
1416
1417 let predicted_id = self.active_window().terminal_manager.next_terminal_id();
1419 self.active_window_mut()
1420 .terminal_log_files
1421 .insert(predicted_id, log_path.clone());
1422 self.active_window_mut()
1423 .terminal_backing_files
1424 .insert(predicted_id, backing_path.clone());
1425
1426 let wrapper_for_spawn = self.resolved_terminal_wrapper();
1428 let terminal_id = match self
1429 .windows
1430 .get_mut(&self.active_window)
1431 .map(|w| &mut w.terminal_manager)
1432 .expect("active window present")
1433 .spawn(
1434 terminal.cols,
1435 terminal.rows,
1436 terminal.cwd.clone(),
1437 Some(log_path.clone()),
1438 Some(backing_path.clone()),
1439 wrapper_for_spawn,
1440 ) {
1441 Ok(id) => id,
1442 Err(e) => {
1443 tracing::warn!(
1444 "Failed to restore terminal {}: {}",
1445 terminal.terminal_index,
1446 e
1447 );
1448 return None;
1449 }
1450 };
1451
1452 if terminal_id != predicted_id {
1454 self.active_window_mut()
1455 .terminal_log_files
1456 .insert(terminal_id, log_path.clone());
1457 self.active_window_mut()
1458 .terminal_backing_files
1459 .insert(terminal_id, backing_path.clone());
1460 self.active_window_mut()
1461 .terminal_log_files
1462 .remove(&predicted_id);
1463 self.active_window_mut()
1464 .terminal_backing_files
1465 .remove(&predicted_id);
1466 }
1467
1468 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1470
1471 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
1474
1475 Some(buffer_id)
1476 }
1477
1478 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
1483 if !backing_path.exists() {
1485 return;
1486 }
1487
1488 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1489 if let Ok(new_state) = EditorState::from_file_with_languages(
1490 backing_path,
1491 self.terminal_width,
1492 self.terminal_height,
1493 large_file_threshold,
1494 &self.grammar_registry,
1495 &self.config.languages,
1496 std::sync::Arc::clone(&self.authority.filesystem),
1497 ) {
1498 self.active_window_mut()
1499 .install_terminal_buffer_state(buffer_id, new_state);
1500 }
1501 }
1502
1503 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
1505 for (buffer_id, metadata) in &self.active_window().buffer_metadata {
1507 if let Some(file_path) = metadata.file_path() {
1508 if file_path == path {
1509 return Ok(*buffer_id);
1510 }
1511 }
1512 }
1513
1514 self.open_file(path).map_err(WorkspaceError::Io)
1516 }
1517
1518 #[allow(clippy::too_many_arguments)]
1520 fn restore_split_node(
1521 &mut self,
1522 node: &SerializedSplitNode,
1523 path_to_buffer: &HashMap<PathBuf, BufferId>,
1524 terminal_buffers: &HashMap<usize, BufferId>,
1525 unnamed_buffers: &HashMap<String, BufferId>,
1526 split_states: &HashMap<usize, SerializedSplitViewState>,
1527 split_id_map: &mut HashMap<usize, SplitId>,
1528 is_first_leaf: bool,
1529 ) {
1530 match node {
1531 SerializedSplitNode::Leaf {
1532 file_path,
1533 split_id,
1534 label,
1535 unnamed_recovery_id,
1536 role,
1537 } => {
1538 let buffer_id = file_path
1540 .as_ref()
1541 .and_then(|p| path_to_buffer.get(p).copied())
1542 .or_else(|| {
1543 unnamed_recovery_id
1544 .as_ref()
1545 .and_then(|id| unnamed_buffers.get(id).copied())
1546 })
1547 .unwrap_or(self.active_buffer());
1548
1549 let current_leaf_id = if is_first_leaf {
1550 let leaf_id = self
1552 .windows
1553 .get(&self.active_window)
1554 .and_then(|w| w.buffers.splits())
1555 .map(|(mgr, _)| mgr)
1556 .expect("active window must have a populated split layout")
1557 .active_split();
1558 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
1559 leaf_id
1560 } else {
1561 self.windows
1563 .get(&self.active_window)
1564 .and_then(|w| w.buffers.splits())
1565 .map(|(mgr, _)| mgr)
1566 .expect("active window must have a populated split layout")
1567 .active_split()
1568 };
1569
1570 split_id_map.insert(*split_id, current_leaf_id.into());
1572
1573 if let Some(label) = label {
1575 self.windows
1576 .get_mut(&self.active_window)
1577 .and_then(|w| w.split_manager_mut())
1578 .expect("active window must have a populated split layout")
1579 .set_label(current_leaf_id, label.clone());
1580 }
1581
1582 if let Some(role) = role {
1585 self.windows
1586 .get_mut(&self.active_window)
1587 .and_then(|w| w.split_manager_mut())
1588 .expect("active window must have a populated split layout")
1589 .clear_role(*role);
1590 self.windows
1591 .get_mut(&self.active_window)
1592 .and_then(|w| w.split_manager_mut())
1593 .expect("active window must have a populated split layout")
1594 .set_leaf_role(current_leaf_id, Some(*role));
1595 }
1596
1597 self.restore_split_view_state(
1599 current_leaf_id,
1600 *split_id,
1601 split_states,
1602 path_to_buffer,
1603 terminal_buffers,
1604 unnamed_buffers,
1605 );
1606 }
1607 SerializedSplitNode::Terminal {
1608 terminal_index,
1609 split_id,
1610 label,
1611 role,
1612 } => {
1613 let buffer_id = terminal_buffers
1614 .get(terminal_index)
1615 .copied()
1616 .unwrap_or(self.active_buffer());
1617
1618 let current_leaf_id = if is_first_leaf {
1619 let leaf_id = self
1620 .windows
1621 .get(&self.active_window)
1622 .and_then(|w| w.buffers.splits())
1623 .map(|(mgr, _)| mgr)
1624 .expect("active window must have a populated split layout")
1625 .active_split();
1626 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
1627 leaf_id
1628 } else {
1629 self.windows
1630 .get(&self.active_window)
1631 .and_then(|w| w.buffers.splits())
1632 .map(|(mgr, _)| mgr)
1633 .expect("active window must have a populated split layout")
1634 .active_split()
1635 };
1636
1637 split_id_map.insert(*split_id, current_leaf_id.into());
1638
1639 if let Some(label) = label {
1641 self.windows
1642 .get_mut(&self.active_window)
1643 .and_then(|w| w.split_manager_mut())
1644 .expect("active window must have a populated split layout")
1645 .set_label(current_leaf_id, label.clone());
1646 }
1647
1648 if let Some(role) = role {
1651 self.windows
1652 .get_mut(&self.active_window)
1653 .and_then(|w| w.split_manager_mut())
1654 .expect("active window must have a populated split layout")
1655 .clear_role(*role);
1656 self.windows
1657 .get_mut(&self.active_window)
1658 .and_then(|w| w.split_manager_mut())
1659 .expect("active window must have a populated split layout")
1660 .set_leaf_role(current_leaf_id, Some(*role));
1661 }
1662
1663 self.windows
1664 .get_mut(&self.active_window)
1665 .and_then(|w| w.split_manager_mut())
1666 .expect("active window must have a populated split layout")
1667 .set_split_buffer(current_leaf_id, buffer_id);
1668
1669 self.restore_split_view_state(
1670 current_leaf_id,
1671 *split_id,
1672 split_states,
1673 path_to_buffer,
1674 terminal_buffers,
1675 unnamed_buffers,
1676 );
1677 }
1678 SerializedSplitNode::Split {
1679 direction,
1680 first,
1681 second,
1682 ratio,
1683 split_id,
1684 } => {
1685 self.restore_split_node(
1687 first,
1688 path_to_buffer,
1689 terminal_buffers,
1690 unnamed_buffers,
1691 split_states,
1692 split_id_map,
1693 is_first_leaf,
1694 );
1695
1696 let second_buffer_id = get_first_leaf_buffer(
1698 second,
1699 path_to_buffer,
1700 terminal_buffers,
1701 unnamed_buffers,
1702 )
1703 .unwrap_or(self.active_buffer());
1704
1705 let split_direction = match direction {
1707 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1708 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1709 };
1710
1711 match self.split_manager_mut().split_active(
1713 split_direction,
1714 second_buffer_id,
1715 *ratio,
1716 ) {
1717 Ok(new_leaf_id) => {
1718 let mut view_state = SplitViewState::with_buffer(
1720 self.terminal_width,
1721 self.terminal_height,
1722 second_buffer_id,
1723 );
1724 view_state.apply_config_defaults(
1725 self.config.editor.line_numbers,
1726 self.config.editor.highlight_current_line,
1727 self.active_window()
1728 .resolve_line_wrap_for_buffer(second_buffer_id),
1729 self.config.editor.wrap_indent,
1730 self.active_window()
1731 .resolve_wrap_column_for_buffer(second_buffer_id),
1732 self.config.editor.rulers.clone(),
1733 );
1734 self.windows
1735 .get_mut(&self.active_window)
1736 .and_then(|w| w.split_view_states_mut())
1737 .expect("active window must have a populated split layout")
1738 .insert(new_leaf_id, view_state);
1739
1740 split_id_map.insert(*split_id, new_leaf_id.into());
1742
1743 self.restore_split_node(
1745 second,
1746 path_to_buffer,
1747 terminal_buffers,
1748 unnamed_buffers,
1749 split_states,
1750 split_id_map,
1751 false,
1752 );
1753 }
1754 Err(e) => {
1755 tracing::error!("Failed to create split during workspace restore: {}", e);
1756 }
1757 }
1758 }
1759 }
1760 }
1761
1762 fn restore_split_view_state(
1764 &mut self,
1765 current_split_id: LeafId,
1766 saved_split_id: usize,
1767 split_states: &HashMap<usize, SerializedSplitViewState>,
1768 path_to_buffer: &HashMap<PathBuf, BufferId>,
1769 terminal_buffers: &HashMap<usize, BufferId>,
1770 unnamed_buffers: &HashMap<String, BufferId>,
1771 ) {
1772 let Some(split_state) = split_states.get(&saved_split_id) else {
1774 return;
1775 };
1776
1777 let split_buf_for_current = self.split_manager().buffer_for_split(current_split_id);
1781 let active_id = self.active_window;
1782 let active_buffer_id = self
1783 .windows
1784 .get_mut(&active_id)
1785 .expect("active window must exist")
1786 .buffers
1787 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1788 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1789 return None;
1790 };
1791 let mut active_buffer_id: Option<BufferId> = None;
1792 if !split_state.open_tabs.is_empty() {
1793 view_state.open_buffers.clear();
1796
1797 for tab in &split_state.open_tabs {
1798 match tab {
1799 SerializedTabRef::File(rel_path) => {
1800 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1801 if !view_state.has_buffer(buffer_id) {
1802 view_state.add_buffer(buffer_id);
1803 }
1804 view_state.ensure_buffer_state(buffer_id);
1806 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1807 view_state
1808 .buffer_state_mut(buffer_id)
1809 .unwrap()
1810 .viewport
1811 .line_wrap_enabled = false;
1812 }
1813 }
1814 }
1815 SerializedTabRef::Terminal(index) => {
1816 if let Some(&buffer_id) = terminal_buffers.get(index) {
1817 if !view_state.has_buffer(buffer_id) {
1818 view_state.add_buffer(buffer_id);
1819 }
1820 view_state
1821 .ensure_buffer_state(buffer_id)
1822 .viewport
1823 .line_wrap_enabled = false;
1824 }
1825 }
1826 SerializedTabRef::Unnamed(recovery_id) => {
1827 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1828 if !view_state.has_buffer(buffer_id) {
1829 view_state.add_buffer(buffer_id);
1830 }
1831 view_state.ensure_buffer_state(buffer_id);
1832 }
1833 }
1834 }
1835 }
1836
1837 if view_state.open_buffers.is_empty() {
1842 if let Some(buf) = split_buf_for_current {
1843 view_state.add_buffer(buf);
1844 view_state.ensure_buffer_state(buf);
1845 }
1846 }
1847
1848 if let Some(active_idx) = split_state.active_tab_index {
1849 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1850 active_buffer_id = match tab {
1851 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1852 SerializedTabRef::Terminal(index) => {
1853 terminal_buffers.get(index).copied()
1854 }
1855 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1856 };
1857 }
1858 }
1859 } else {
1860 for rel_path in &split_state.open_files {
1862 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1863 if !view_state.has_buffer(buffer_id) {
1864 view_state.add_buffer(buffer_id);
1865 }
1866 view_state.ensure_buffer_state(buffer_id);
1867 }
1868 }
1869
1870 let active_file_path =
1871 split_state.open_files.get(split_state.active_file_index);
1872 active_buffer_id =
1873 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1874 }
1875
1876 for (rel_path, file_state) in &split_state.file_states {
1878 let rel_str = rel_path.to_string_lossy();
1880 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1881 match unnamed_buffers.get(recovery_id).copied() {
1882 Some(id) => id,
1883 None => continue,
1884 }
1885 } else {
1886 match path_to_buffer.get(rel_path).copied() {
1887 Some(id) => id,
1888 None => continue,
1889 }
1890 };
1891 let max_pos = __buffers_mut
1892 .get(&buffer_id)
1893 .map(|b| b.buffer.len())
1894 .unwrap_or(0);
1895
1896 let buf_state = view_state.ensure_buffer_state(buffer_id);
1898
1899 let cursor_pos = file_state.cursor.position.min(max_pos);
1900 buf_state.cursors.primary_mut().position = cursor_pos;
1901 buf_state.cursors.primary_mut().anchor =
1902 file_state.cursor.anchor.map(|a| a.min(max_pos));
1903 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1904
1905 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1906 buf_state.viewport.top_view_line_offset =
1907 file_state.scroll.top_view_line_offset;
1908 buf_state.viewport.left_column = file_state.scroll.left_column;
1909 buf_state.viewport.set_skip_resize_sync();
1910
1911 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1919 super::navigation::reconcile_restored_buffer_view(
1920 buf_state,
1921 &mut state.buffer,
1922 );
1923 }
1924
1925 buf_state.view_mode = match file_state.view_mode {
1927 SerializedViewMode::Source => ViewMode::Source,
1928 SerializedViewMode::PageView => ViewMode::PageView,
1929 };
1930 buf_state.compose_width = file_state.compose_width;
1931 buf_state.plugin_state = file_state.plugin_state.clone();
1932 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1933 buf_state.folds.clear(&mut state.marker_list);
1934 for fold in &file_state.folds {
1935 let Some(resolved_header) = resolve_fold_header_line(
1942 &state.buffer,
1943 fold.header_line,
1944 fold.header_text.as_deref(),
1945 ) else {
1946 tracing::debug!(
1947 "Dropping stale fold: header_line={} no longer matches stored \
1948 header_text after external edit",
1949 fold.header_line,
1950 );
1951 continue;
1952 };
1953
1954 let shift = resolved_header as i64 - fold.header_line as i64;
1956 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1957 let start_line = resolved_header.saturating_add(1);
1958 let end_line = adjusted_end;
1959 if start_line > end_line {
1960 continue;
1961 }
1962 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1963 else {
1964 continue;
1965 };
1966 let end_byte = state
1967 .buffer
1968 .line_start_offset(end_line.saturating_add(1))
1969 .unwrap_or_else(|| state.buffer.len());
1970 buf_state.folds.add(
1971 &mut state.marker_list,
1972 start_byte,
1973 end_byte,
1974 fold.placeholder.clone(),
1975 );
1976 }
1977 }
1978
1979 tracing::trace!(
1980 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1981 rel_path,
1982 cursor_pos,
1983 buf_state.viewport.top_byte,
1984 buf_state.view_mode,
1985 );
1986 }
1987
1988 let restored_view_mode = match split_state.view_mode {
1991 SerializedViewMode::Source => ViewMode::Source,
1992 SerializedViewMode::PageView => ViewMode::PageView,
1993 };
1994
1995 if let Some(active_buf_id) = active_buffer_id {
1996 view_state.switch_buffer(active_buf_id);
1998
1999 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
2001 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
2002 });
2003 if !active_has_file_state {
2004 view_state.active_state_mut().view_mode = restored_view_mode.clone();
2005 view_state.active_state_mut().compose_width = split_state.compose_width;
2006 }
2007
2008 }
2010 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
2011 active_buffer_id
2012 })
2013 .flatten();
2014
2015 if let Some(active_buf_id) = active_buffer_id {
2019 self.windows
2020 .get_mut(&active_id)
2021 .and_then(|w| w.split_manager_mut())
2022 .expect("active window must have a populated split layout")
2023 .set_split_buffer(current_split_id, active_buf_id);
2024 }
2025 }
2026
2027 pub fn save_all_windows_workspaces(&mut self) -> Result<(), WorkspaceError> {
2040 let originally_active = self.active_window;
2041 let originally_wd = self.working_dir.clone();
2042
2043 let targets: Vec<(fresh_core::WindowId, PathBuf)> = self
2044 .windows
2045 .iter()
2046 .filter(|(_, w)| w.buffers.splits().is_some())
2047 .map(|(id, w)| (*id, w.root.clone()))
2048 .collect();
2049
2050 let mut first_err = None;
2051 for (id, root) in targets {
2052 self.active_window = id;
2053 self.working_dir = root;
2054 if let Err(e) = self.save_workspace() {
2055 tracing::warn!("Failed to save workspace for window {id}: {e}");
2056 if first_err.is_none() {
2057 first_err = Some(e);
2058 }
2059 }
2060 }
2061
2062 self.active_window = originally_active;
2063 self.working_dir = originally_wd;
2064
2065 match first_err {
2066 Some(e) => Err(e),
2067 None => Ok(()),
2068 }
2069 }
2070
2071 pub fn restore_inactive_window_workspaces(&mut self) {
2087 let originally_active = self.active_window;
2088 let originally_wd = self.working_dir.clone();
2089 let saved_plugin_state = self.plugin_global_state.clone();
2090
2091 let targets: Vec<(fresh_core::WindowId, PathBuf)> = self
2092 .windows
2093 .iter()
2094 .filter(|(id, _)| **id != originally_active)
2095 .map(|(id, w)| (*id, w.root.clone()))
2096 .collect();
2097
2098 for (id, root) in targets {
2099 self.active_window = id;
2100 self.working_dir = root;
2101 match self.try_restore_workspace() {
2102 Ok(true) => tracing::debug!("Restored workspace for inactive window {id}"),
2103 Ok(false) => tracing::trace!(
2104 "No persisted workspace for inactive window {id}; seed layout kept"
2105 ),
2106 Err(e) => {
2107 tracing::warn!("Failed to restore workspace for inactive window {id}: {e}")
2108 }
2109 }
2110 }
2111
2112 self.active_window = originally_active;
2113 self.working_dir = originally_wd;
2114 self.plugin_global_state = saved_plugin_state;
2115 }
2116}
2117
2118fn get_first_leaf_buffer(
2120 node: &SerializedSplitNode,
2121 path_to_buffer: &HashMap<PathBuf, BufferId>,
2122 terminal_buffers: &HashMap<usize, BufferId>,
2123 unnamed_buffers: &HashMap<String, BufferId>,
2124) -> Option<BufferId> {
2125 match node {
2126 SerializedSplitNode::Leaf {
2127 file_path,
2128 unnamed_recovery_id,
2129 ..
2130 } => file_path
2131 .as_ref()
2132 .and_then(|p| path_to_buffer.get(p).copied())
2133 .or_else(|| {
2134 unnamed_recovery_id
2135 .as_ref()
2136 .and_then(|id| unnamed_buffers.get(id).copied())
2137 }),
2138 SerializedSplitNode::Terminal { terminal_index, .. } => {
2139 terminal_buffers.get(terminal_index).copied()
2140 }
2141 SerializedSplitNode::Split { first, .. } => {
2142 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2143 }
2144 }
2145}
2146
2147fn serialize_split_node(
2152 node: &SplitNode,
2153 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2154 working_dir: &Path,
2155 terminal_buffers: &HashMap<BufferId, TerminalId>,
2156 terminal_indices: &HashMap<TerminalId, usize>,
2157 split_labels: &HashMap<SplitId, String>,
2158) -> SerializedSplitNode {
2159 serialize_split_node_pruned(
2160 node,
2161 buffer_metadata,
2162 working_dir,
2163 terminal_buffers,
2164 terminal_indices,
2165 split_labels,
2166 )
2167 .unwrap_or({
2168 SerializedSplitNode::Leaf {
2171 file_path: None,
2172 split_id: 0,
2173 label: None,
2174 unnamed_recovery_id: None,
2175 role: None,
2176 }
2177 })
2178}
2179
2180fn serialize_split_node_pruned(
2187 node: &SplitNode,
2188 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2189 working_dir: &Path,
2190 terminal_buffers: &HashMap<BufferId, TerminalId>,
2191 terminal_indices: &HashMap<TerminalId, usize>,
2192 split_labels: &HashMap<SplitId, String>,
2193) -> Option<SerializedSplitNode> {
2194 match node {
2195 SplitNode::Grouped { layout, .. } => {
2196 serialize_split_node_pruned(
2200 layout,
2201 buffer_metadata,
2202 working_dir,
2203 terminal_buffers,
2204 terminal_indices,
2205 split_labels,
2206 )
2207 }
2208 SplitNode::Leaf {
2209 buffer_id,
2210 split_id,
2211 role,
2212 } => {
2213 let raw_split_id: SplitId = (*split_id).into();
2214 let label = split_labels.get(&raw_split_id).cloned();
2215 let role = *role;
2216
2217 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2218 if let Some(index) = terminal_indices.get(terminal_id) {
2219 return Some(SerializedSplitNode::Terminal {
2220 terminal_index: *index,
2221 split_id: raw_split_id.0,
2222 label,
2223 role,
2224 });
2225 }
2226 }
2227
2228 let meta = buffer_metadata.get(buffer_id);
2229
2230 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2234 return None;
2235 }
2236
2237 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2238 if abs_path.as_os_str().is_empty() {
2239 None } else {
2241 abs_path
2242 .strip_prefix(working_dir)
2243 .ok()
2244 .map(|p| p.to_path_buf())
2245 }
2246 });
2247
2248 let unnamed_recovery_id = if file_path.is_none() {
2251 meta.and_then(|m| m.recovery_id.clone())
2252 } else {
2253 None
2254 };
2255
2256 Some(SerializedSplitNode::Leaf {
2257 file_path,
2258 split_id: raw_split_id.0,
2259 label,
2260 unnamed_recovery_id,
2261 role,
2262 })
2263 }
2264 SplitNode::Split {
2265 direction,
2266 first,
2267 second,
2268 ratio,
2269 split_id,
2270 ..
2271 } => {
2272 let raw_split_id: SplitId = (*split_id).into();
2273 let first = serialize_split_node_pruned(
2274 first,
2275 buffer_metadata,
2276 working_dir,
2277 terminal_buffers,
2278 terminal_indices,
2279 split_labels,
2280 );
2281 let second = serialize_split_node_pruned(
2282 second,
2283 buffer_metadata,
2284 working_dir,
2285 terminal_buffers,
2286 terminal_indices,
2287 split_labels,
2288 );
2289 match (first, second) {
2290 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2291 direction: match direction {
2292 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2293 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2294 },
2295 first: Box::new(f),
2296 second: Box::new(s),
2297 ratio: *ratio,
2298 split_id: raw_split_id.0,
2299 }),
2300 (Some(only), None) | (None, Some(only)) => Some(only),
2303 (None, None) => None,
2304 }
2305 }
2306 }
2307}
2308
2309fn serialize_split_view_state(
2310 view_state: &crate::view::split::SplitViewState,
2311 buffers: &HashMap<BufferId, EditorState>,
2312 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2313 working_dir: &Path,
2314 active_buffer: Option<BufferId>,
2315 terminal_buffers: &HashMap<BufferId, TerminalId>,
2316 terminal_indices: &HashMap<TerminalId, usize>,
2317) -> SerializedSplitViewState {
2318 let mut open_tabs = Vec::new();
2319 let mut open_files = Vec::new();
2320 let mut active_tab_index = None;
2321
2322 for buffer_id in view_state.buffer_tab_ids() {
2324 let buffer_id = &buffer_id;
2325 let tab_index = open_tabs.len();
2326 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2327 if let Some(idx) = terminal_indices.get(terminal_id) {
2328 open_tabs.push(SerializedTabRef::Terminal(*idx));
2329 if Some(*buffer_id) == active_buffer {
2330 active_tab_index = Some(tab_index);
2331 }
2332 continue;
2333 }
2334 }
2335
2336 if let Some(meta) = buffer_metadata.get(buffer_id) {
2337 if let Some(abs_path) = meta.file_path() {
2338 if abs_path.as_os_str().is_empty() {
2339 if let Some(ref recovery_id) = meta.recovery_id {
2341 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2342 if Some(*buffer_id) == active_buffer {
2343 active_tab_index = Some(tab_index);
2344 }
2345 }
2346 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2347 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2348 open_files.push(rel_path.to_path_buf());
2349 if Some(*buffer_id) == active_buffer {
2350 active_tab_index = Some(tab_index);
2351 }
2352 } else {
2353 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2355 if Some(*buffer_id) == active_buffer {
2356 active_tab_index = Some(tab_index);
2357 }
2358 }
2359 }
2360 }
2361 }
2362
2363 let active_file_index = active_tab_index
2365 .and_then(|idx| open_tabs.get(idx))
2366 .and_then(|tab| match tab {
2367 SerializedTabRef::File(path) => {
2368 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2369 }
2370 _ => None,
2371 })
2372 .unwrap_or(0);
2373
2374 let mut file_states = HashMap::new();
2376 for (buffer_id, buf_state) in &view_state.keyed_states {
2377 let Some(meta) = buffer_metadata.get(buffer_id) else {
2378 continue;
2379 };
2380 let Some(abs_path) = meta.file_path() else {
2381 continue;
2382 };
2383
2384 let state_key = if abs_path.as_os_str().is_empty() {
2386 if let Some(ref recovery_id) = meta.recovery_id {
2388 PathBuf::from(format!("__unnamed__{}", recovery_id))
2389 } else {
2390 continue;
2391 }
2392 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2393 rp.to_path_buf()
2394 } else {
2395 abs_path.to_path_buf()
2397 };
2398
2399 let primary_cursor = buf_state.cursors.primary();
2400 let folds = buffers
2401 .get(buffer_id)
2402 .map(|state| {
2403 buf_state
2404 .folds
2405 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2406 .into_iter()
2407 .map(|range| SerializedFoldRange {
2408 header_line: range.header_line,
2409 end_line: range.end_line,
2410 placeholder: range.placeholder,
2411 header_text: range.header_text,
2412 })
2413 .collect::<Vec<_>>()
2414 })
2415 .unwrap_or_default();
2416
2417 file_states.insert(
2418 state_key,
2419 SerializedFileState {
2420 cursor: SerializedCursor {
2421 position: primary_cursor.position,
2422 anchor: primary_cursor.anchor,
2423 sticky_column: primary_cursor.sticky_column,
2424 },
2425 additional_cursors: buf_state
2426 .cursors
2427 .iter()
2428 .skip(1) .map(|(_, cursor)| SerializedCursor {
2430 position: cursor.position,
2431 anchor: cursor.anchor,
2432 sticky_column: cursor.sticky_column,
2433 })
2434 .collect(),
2435 scroll: SerializedScroll {
2436 top_byte: buf_state.viewport.top_byte,
2437 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2438 left_column: buf_state.viewport.left_column,
2439 },
2440 view_mode: match buf_state.view_mode {
2441 ViewMode::Source => SerializedViewMode::Source,
2442 ViewMode::PageView => SerializedViewMode::PageView,
2443 },
2444 compose_width: buf_state.compose_width,
2445 plugin_state: buf_state.plugin_state.clone(),
2446 folds,
2447 },
2448 );
2449 }
2450
2451 let active_view_mode = active_buffer
2453 .and_then(|id| view_state.keyed_states.get(&id))
2454 .map(|bs| match bs.view_mode {
2455 ViewMode::Source => SerializedViewMode::Source,
2456 ViewMode::PageView => SerializedViewMode::PageView,
2457 })
2458 .unwrap_or(SerializedViewMode::Source);
2459 let active_compose_width = active_buffer
2460 .and_then(|id| view_state.keyed_states.get(&id))
2461 .and_then(|bs| bs.compose_width);
2462
2463 SerializedSplitViewState {
2464 open_tabs,
2465 active_tab_index,
2466 open_files,
2467 active_file_index,
2468 file_states,
2469 tab_scroll_offset: view_state.tab_scroll_offset,
2470 view_mode: active_view_mode,
2471 compose_width: active_compose_width,
2472 }
2473}
2474
2475fn serialize_bookmarks(
2476 bookmarks: &BookmarkState,
2477 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2478 working_dir: &Path,
2479) -> HashMap<char, SerializedBookmark> {
2480 bookmarks
2481 .iter()
2482 .filter_map(|(key, bookmark)| {
2483 buffer_metadata
2484 .get(&bookmark.buffer_id)
2485 .and_then(|meta| meta.file_path())
2486 .and_then(|abs_path| {
2487 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2488 (
2489 key,
2490 SerializedBookmark {
2491 file_path: rel_path.to_path_buf(),
2492 position: bookmark.position,
2493 },
2494 )
2495 })
2496 })
2497 })
2498 .collect()
2499}
2500
2501fn collect_file_paths_from_states(
2503 split_states: &HashMap<usize, SerializedSplitViewState>,
2504) -> Vec<PathBuf> {
2505 let mut paths = Vec::new();
2506 for state in split_states.values() {
2507 if !state.open_tabs.is_empty() {
2508 for tab in &state.open_tabs {
2509 if let SerializedTabRef::File(path) = tab {
2510 if !paths.contains(path) {
2511 paths.push(path.clone());
2512 }
2513 }
2514 }
2515 } else {
2516 for path in &state.open_files {
2517 if !paths.contains(path) {
2518 paths.push(path.clone());
2519 }
2520 }
2521 }
2522 }
2523 paths
2524}
2525
2526fn get_expanded_dirs(
2528 explorer: &crate::view::file_tree::FileTreeView,
2529 working_dir: &Path,
2530) -> Vec<PathBuf> {
2531 let mut expanded = Vec::new();
2532 let tree = explorer.tree();
2533
2534 for node in tree.all_nodes() {
2536 if node.is_expanded() && node.is_dir() {
2537 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2539 expanded.push(rel_path.to_path_buf());
2540 }
2541 }
2542 }
2543
2544 expanded
2545}