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::types::Bookmark;
44use super::Editor;
45
46pub struct WorkspaceTracker {
50 dirty: bool,
52 last_save: Instant,
54 save_interval: std::time::Duration,
56 enabled: bool,
58}
59
60impl WorkspaceTracker {
61 pub fn new(enabled: bool) -> Self {
63 Self {
64 dirty: false,
65 last_save: Instant::now(),
66 save_interval: std::time::Duration::from_secs(5),
67 enabled,
68 }
69 }
70
71 pub fn is_enabled(&self) -> bool {
73 self.enabled
74 }
75
76 pub fn mark_dirty(&mut self) {
78 if self.enabled {
79 self.dirty = true;
80 }
81 }
82
83 pub fn should_save(&self) -> bool {
85 self.enabled && self.dirty && self.last_save.elapsed() >= self.save_interval
86 }
87
88 pub fn record_save(&mut self) {
90 self.dirty = false;
91 self.last_save = Instant::now();
92 }
93
94 pub fn is_dirty(&self) -> bool {
96 self.dirty
97 }
98}
99
100impl Editor {
101 pub fn capture_workspace(&self) -> Workspace {
103 tracing::debug!("Capturing workspace for {:?}", self.working_dir);
104
105 let mut terminals = Vec::new();
107 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
108 let mut seen = HashSet::new();
109 for terminal_id in self.terminal_buffers.values().copied() {
110 if seen.insert(terminal_id) {
111 let idx = terminals.len();
112 terminal_indices.insert(terminal_id, idx);
113 let handle = self.terminal_manager.get(terminal_id);
114 let (cols, rows) = handle
115 .map(|h| h.size())
116 .unwrap_or((self.terminal_width, self.terminal_height));
117 let cwd = handle.and_then(|h| h.cwd());
118 let shell = handle
119 .map(|h| h.shell().to_string())
120 .unwrap_or_else(crate::services::terminal::detect_shell);
121 let log_path = self
122 .terminal_log_files
123 .get(&terminal_id)
124 .cloned()
125 .unwrap_or_else(|| {
126 let root = self.dir_context.terminal_dir_for(&self.working_dir);
127 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
128 });
129 let backing_path = self
130 .terminal_backing_files
131 .get(&terminal_id)
132 .cloned()
133 .unwrap_or_else(|| {
134 let root = self.dir_context.terminal_dir_for(&self.working_dir);
135 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
136 });
137
138 terminals.push(SerializedTerminalWorkspace {
139 terminal_index: idx,
140 cwd,
141 shell,
142 cols,
143 rows,
144 log_path,
145 backing_path,
146 });
147 }
148 }
149
150 let split_layout = serialize_split_node(
151 self.split_manager.root(),
152 &self.buffer_metadata,
153 &self.working_dir,
154 &self.terminal_buffers,
155 &terminal_indices,
156 self.split_manager.labels(),
157 );
158
159 let active_buffers: HashMap<LeafId, BufferId> = self
162 .split_manager
163 .root()
164 .get_leaves_with_rects(ratatui::layout::Rect::default())
165 .into_iter()
166 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
167 .collect();
168
169 let mut split_states = HashMap::new();
170 for (leaf_id, view_state) in &self.split_view_states {
171 let active_buffer = active_buffers.get(leaf_id).copied();
172 let serialized = serialize_split_view_state(
173 view_state,
174 &self.buffers,
175 &self.buffer_metadata,
176 &self.working_dir,
177 active_buffer,
178 &self.terminal_buffers,
179 &terminal_indices,
180 );
181 tracing::trace!(
182 "Split {:?}: {} open tabs, active_buffer={:?}",
183 leaf_id,
184 serialized.open_tabs.len(),
185 active_buffer
186 );
187 split_states.insert(leaf_id.0 .0, serialized);
188 }
189
190 tracing::debug!(
191 "Captured {} split states, active_split={}",
192 split_states.len(),
193 SplitId::from(self.split_manager.active_split()).0
194 );
195
196 let file_explorer = if let Some(ref explorer) = self.file_explorer {
198 let expanded_dirs = get_expanded_dirs(explorer, &self.working_dir);
200 FileExplorerState {
201 visible: self.file_explorer_visible,
202 width_percent: self.file_explorer_width_percent,
203 expanded_dirs,
204 scroll_offset: explorer.get_scroll_offset(),
205 show_hidden: explorer.ignore_patterns().show_hidden(),
206 show_gitignored: explorer.ignore_patterns().show_gitignored(),
207 }
208 } else {
209 FileExplorerState {
210 visible: self.file_explorer_visible,
211 width_percent: self.file_explorer_width_percent,
212 expanded_dirs: Vec::new(),
213 scroll_offset: 0,
214 show_hidden: false,
215 show_gitignored: false,
216 }
217 };
218
219 let config_overrides = WorkspaceConfigOverrides {
221 line_numbers: Some(self.config.editor.line_numbers),
222 relative_line_numbers: Some(self.config.editor.relative_line_numbers),
223 line_wrap: Some(self.config.editor.line_wrap),
224 syntax_highlighting: Some(self.config.editor.syntax_highlighting),
225 enable_inlay_hints: Some(self.config.editor.enable_inlay_hints),
226 mouse_enabled: Some(self.mouse_enabled),
227 menu_bar_hidden: Some(!self.menu_bar_visible),
228 };
229
230 let histories = WorkspaceHistories {
232 search: self
233 .prompt_histories
234 .get("search")
235 .map(|h| h.items().to_vec())
236 .unwrap_or_default(),
237 replace: self
238 .prompt_histories
239 .get("replace")
240 .map(|h| h.items().to_vec())
241 .unwrap_or_default(),
242 command_palette: Vec::new(), goto_line: self
244 .prompt_histories
245 .get("goto_line")
246 .map(|h| h.items().to_vec())
247 .unwrap_or_default(),
248 open_file: Vec::new(), };
250 tracing::trace!(
251 "Captured histories: {} search, {} replace",
252 histories.search.len(),
253 histories.replace.len()
254 );
255
256 let search_options = SearchOptions {
258 case_sensitive: self.search_case_sensitive,
259 whole_word: self.search_whole_word,
260 use_regex: self.search_use_regex,
261 confirm_each: self.search_confirm_each,
262 };
263
264 let bookmarks =
266 serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.working_dir);
267
268 let external_files: Vec<PathBuf> = self
271 .buffer_metadata
272 .values()
273 .filter_map(|meta| meta.file_path())
274 .filter(|abs_path| abs_path.strip_prefix(&self.working_dir).is_err())
275 .cloned()
276 .collect();
277 if !external_files.is_empty() {
278 tracing::debug!("Captured {} external files", external_files.len());
279 }
280
281 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.config.editor.hot_exit {
283 self.buffer_metadata
284 .iter()
285 .filter_map(|(buffer_id, meta)| {
286 let path = meta.file_path()?;
288 if !path.as_os_str().is_empty() {
289 return None;
290 }
291 if meta.hidden_from_tabs || meta.is_virtual() {
293 return None;
294 }
295 let state = self.buffers.get(buffer_id)?;
297 if state.buffer.total_bytes() == 0 {
298 return None;
299 }
300 let recovery_id = meta.recovery_id.clone()?;
302 Some(UnnamedBufferRef {
303 recovery_id,
304 display_name: meta.display_name.clone(),
305 })
306 })
307 .collect()
308 } else {
309 Vec::new()
310 };
311 if !unnamed_buffers.is_empty() {
312 tracing::debug!("Captured {} unnamed buffers", unnamed_buffers.len());
313 }
314
315 Workspace {
316 version: WORKSPACE_VERSION,
317 working_dir: self.working_dir.clone(),
318 split_layout,
319 active_split_id: SplitId::from(self.split_manager.active_split()).0,
320 split_states,
321 config_overrides,
322 file_explorer,
323 histories,
324 search_options,
325 bookmarks,
326 terminals,
327 external_files,
328 unnamed_buffers,
329 plugin_global_state: self.plugin_global_state.clone(),
330 saved_at: std::time::SystemTime::now()
331 .duration_since(std::time::UNIX_EPOCH)
332 .unwrap_or_default()
333 .as_secs(),
334 }
335 }
336
337 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
343 self.sync_all_terminal_backing_files();
345
346 self.save_all_global_file_states();
348
349 let workspace = self.capture_workspace();
350
351 if let Some(ref session_name) = self.session_name {
353 workspace.save_session(session_name)
354 } else {
355 workspace.save()
356 }
357 }
358
359 fn save_all_global_file_states(&self) {
361 for (leaf_id, view_state) in &self.split_view_states {
363 let active_buffer = self
365 .split_manager
366 .root()
367 .get_leaves_with_rects(ratatui::layout::Rect::default())
368 .into_iter()
369 .find(|(sid, _, _)| *sid == *leaf_id)
370 .map(|(_, buffer_id, _)| buffer_id);
371
372 if let Some(buffer_id) = active_buffer {
373 self.save_buffer_file_state(buffer_id, view_state);
374 }
375 }
376 }
377
378 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
380 let abs_path = match self.buffer_metadata.get(&buffer_id) {
382 Some(metadata) => match metadata.file_path() {
383 Some(path) => path.to_path_buf(),
384 None => return, },
386 None => return,
387 };
388
389 let primary_cursor = view_state.cursors.primary();
391 let file_state = SerializedFileState {
392 cursor: SerializedCursor {
393 position: primary_cursor.position,
394 anchor: primary_cursor.anchor,
395 sticky_column: primary_cursor.sticky_column,
396 },
397 additional_cursors: view_state
398 .cursors
399 .iter()
400 .skip(1)
401 .map(|(_, cursor)| SerializedCursor {
402 position: cursor.position,
403 anchor: cursor.anchor,
404 sticky_column: cursor.sticky_column,
405 })
406 .collect(),
407 scroll: SerializedScroll {
408 top_byte: view_state.viewport.top_byte,
409 top_view_line_offset: view_state.viewport.top_view_line_offset,
410 left_column: view_state.viewport.left_column,
411 },
412 view_mode: Default::default(),
413 compose_width: None,
414 plugin_state: std::collections::HashMap::new(),
415 folds: Vec::new(),
416 };
417
418 PersistedFileWorkspace::save(&abs_path, file_state);
420 }
421
422 fn sync_all_terminal_backing_files(&mut self) {
427 use std::io::BufWriter;
428
429 let terminals_to_sync: Vec<_> = self
431 .terminal_buffers
432 .values()
433 .copied()
434 .filter_map(|terminal_id| {
435 self.terminal_backing_files
436 .get(&terminal_id)
437 .map(|path| (terminal_id, path.clone()))
438 })
439 .collect();
440
441 for (terminal_id, backing_path) in terminals_to_sync {
442 if let Some(handle) = self.terminal_manager.get(terminal_id) {
443 if let Ok(state) = handle.state.lock() {
444 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_path) {
446 let mut writer = BufWriter::new(&mut *file);
447 if let Err(e) = state.append_visible_screen(&mut writer) {
448 tracing::warn!(
449 "Failed to sync terminal {:?} to backing file: {}",
450 terminal_id,
451 e
452 );
453 }
454 }
455 }
456 }
457 }
458 }
459
460 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
464 tracing::debug!("Attempting to restore workspace for {:?}", self.working_dir);
465
466 let workspace = if let Some(ref session_name) = self.session_name {
468 Workspace::load_session(session_name, &self.working_dir)?
469 } else {
470 Workspace::load(&self.working_dir)?
471 };
472
473 match workspace {
474 Some(workspace) => {
475 tracing::info!("Found workspace, applying...");
476 self.apply_workspace(&workspace)?;
477 Ok(true)
478 }
479 None => {
480 tracing::debug!("No workspace found for {:?}", self.working_dir);
481 Ok(false)
482 }
483 }
484 }
485
486 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
492 if !self.config.editor.hot_exit {
493 return Ok(0);
494 }
495
496 let entries = self.recovery_service.list_recoverable()?;
497 if entries.is_empty() {
498 return Ok(0);
499 }
500
501 let buffer_files: Vec<_> = self
503 .buffers
504 .iter()
505 .filter_map(|(buffer_id, state)| {
506 let path = state.buffer.file_path()?.to_path_buf();
507 if path.as_os_str().is_empty() {
508 return None; }
510 Some((*buffer_id, path))
511 })
512 .collect();
513
514 let mut recovered = 0;
515 for (buffer_id, file_path) in buffer_files {
516 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
517 let entry = entries.iter().find(|e| e.id == recovery_id);
518 if let Some(entry) = entry {
519 match self.recovery_service.load_recovery(entry) {
520 Ok(crate::services::recovery::RecoveryResult::Recovered {
521 content, ..
522 }) => {
523 if let Some(state) = self.buffers.get_mut(&buffer_id) {
524 let current_len = state.buffer.total_bytes();
525 let text = String::from_utf8_lossy(&content).into_owned();
526 let current = state.buffer.get_text_range_mut(0, current_len).ok();
527 let current_text = current
528 .as_ref()
529 .map(|b| String::from_utf8_lossy(b).into_owned());
530 if current_text.as_deref() != Some(&text) {
531 state.buffer.delete(0..current_len);
532 state.buffer.insert(0, &text);
533 state.buffer.set_modified(true);
534 state.buffer.set_recovery_pending(false);
535 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
538 log.clear_saved_position();
539 }
540 recovered += 1;
541 tracing::info!(
542 "Restored unsaved changes for {:?} from hot exit recovery",
543 file_path
544 );
545 }
546 }
547 }
548 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
549 chunks,
550 ..
551 }) => {
552 if let Some(state) = self.buffers.get_mut(&buffer_id) {
553 for chunk in chunks.into_iter().rev() {
554 let text = String::from_utf8_lossy(&chunk.content).into_owned();
555 if chunk.original_len > 0 {
556 state
557 .buffer
558 .delete(chunk.offset..chunk.offset + chunk.original_len);
559 }
560 state.buffer.insert(chunk.offset, &text);
561 }
562 state.buffer.set_modified(true);
563 state.buffer.set_recovery_pending(false);
564 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
567 log.clear_saved_position();
568 }
569 recovered += 1;
570 tracing::info!(
571 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
572 file_path
573 );
574 }
575 }
576 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
577 original_path,
578 ..
579 }) => {
580 let name = original_path
581 .file_name()
582 .unwrap_or_default()
583 .to_string_lossy();
584 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
585 self.set_status_message(format!(
586 "{} changed on disk; unsaved changes not restored",
587 name
588 ));
589 }
590 Ok(_) => {} Err(e) => {
592 tracing::debug!(
593 "Failed to load hot exit recovery for {:?}: {}",
594 file_path,
595 e
596 );
597 }
598 }
599 }
600 }
601
602 Ok(recovered)
603 }
604
605 pub fn apply_workspace(&mut self, workspace: &Workspace) -> Result<(), WorkspaceError> {
607 tracing::debug!(
608 "Applying workspace with {} split states",
609 workspace.split_states.len()
610 );
611
612 if let Some(line_numbers) = workspace.config_overrides.line_numbers {
614 self.config.editor.line_numbers = line_numbers;
615 }
616 if let Some(relative_line_numbers) = workspace.config_overrides.relative_line_numbers {
617 self.config.editor.relative_line_numbers = relative_line_numbers;
618 }
619 if let Some(line_wrap) = workspace.config_overrides.line_wrap {
620 self.config.editor.line_wrap = line_wrap;
621 }
622 if let Some(syntax_highlighting) = workspace.config_overrides.syntax_highlighting {
623 self.config.editor.syntax_highlighting = syntax_highlighting;
624 }
625 if let Some(enable_inlay_hints) = workspace.config_overrides.enable_inlay_hints {
626 self.config.editor.enable_inlay_hints = enable_inlay_hints;
627 }
628 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
629 self.mouse_enabled = mouse_enabled;
630 }
631 if let Some(menu_bar_hidden) = workspace.config_overrides.menu_bar_hidden {
632 self.menu_bar_visible = !menu_bar_hidden;
633 }
634
635 if !workspace.plugin_global_state.is_empty() {
637 tracing::debug!(
638 "Restoring plugin global state for {} plugins",
639 workspace.plugin_global_state.len()
640 );
641 self.plugin_global_state = workspace.plugin_global_state.clone();
642 }
643
644 self.search_case_sensitive = workspace.search_options.case_sensitive;
646 self.search_whole_word = workspace.search_options.whole_word;
647 self.search_use_regex = workspace.search_options.use_regex;
648 self.search_confirm_each = workspace.search_options.confirm_each;
649
650 tracing::debug!(
652 "Restoring histories: {} search, {} replace, {} goto_line",
653 workspace.histories.search.len(),
654 workspace.histories.replace.len(),
655 workspace.histories.goto_line.len()
656 );
657 for item in &workspace.histories.search {
658 self.get_or_create_prompt_history("search")
659 .push(item.clone());
660 }
661 for item in &workspace.histories.replace {
662 self.get_or_create_prompt_history("replace")
663 .push(item.clone());
664 }
665 for item in &workspace.histories.goto_line {
666 self.get_or_create_prompt_history("goto_line")
667 .push(item.clone());
668 }
669
670 self.file_explorer_visible = workspace.file_explorer.visible;
672 self.file_explorer_width_percent = workspace.file_explorer.width_percent;
673
674 if workspace.file_explorer.show_hidden {
677 self.pending_file_explorer_show_hidden = Some(true);
678 }
679 if workspace.file_explorer.show_gitignored {
680 self.pending_file_explorer_show_gitignored = Some(true);
681 }
682
683 if self.file_explorer_visible && self.file_explorer.is_none() {
686 self.init_file_explorer();
687 }
688
689 let file_paths = collect_file_paths_from_states(&workspace.split_states);
692 tracing::debug!(
693 "Workspace has {} files to restore: {:?}",
694 file_paths.len(),
695 file_paths
696 );
697 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
698
699 for rel_path in file_paths {
700 let abs_path = self.working_dir.join(&rel_path);
701 tracing::trace!(
702 "Checking file: {:?} (exists: {})",
703 abs_path,
704 abs_path.exists()
705 );
706 if abs_path.exists() {
707 match self.open_file_internal(&abs_path) {
709 Ok(buffer_id) => {
710 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
711 path_to_buffer.insert(rel_path, buffer_id);
712 }
713 Err(e) => {
714 tracing::warn!("Failed to open file {:?}: {}", abs_path, e);
715 }
716 }
717 } else {
718 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
719 }
720 }
721
722 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
723
724 if !workspace.external_files.is_empty() {
727 tracing::debug!(
728 "Restoring {} external files: {:?}",
729 workspace.external_files.len(),
730 workspace.external_files
731 );
732 for abs_path in &workspace.external_files {
733 if abs_path.exists() {
734 match self.open_file_internal(abs_path) {
735 Ok(buffer_id) => {
736 path_to_buffer.insert(abs_path.clone(), buffer_id);
738 tracing::debug!(
739 "Restored external file {:?} as buffer {:?}",
740 abs_path,
741 buffer_id
742 );
743 }
744 Err(e) => {
745 tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e);
746 }
747 }
748 } else {
749 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
750 }
751 }
752 }
753
754 if self.config.editor.hot_exit {
756 let entries = self.recovery_service.list_recoverable().unwrap_or_default();
757 if !entries.is_empty() {
758 for &buffer_id in path_to_buffer.values() {
759 let file_path = self
760 .buffers
761 .get(&buffer_id)
762 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
763 let file_path = match file_path {
764 Some(p) => p,
765 None => continue,
766 };
767
768 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
770 let entry = entries.iter().find(|e| e.id == recovery_id);
771 if let Some(entry) = entry {
772 match self.recovery_service.load_recovery(entry) {
773 Ok(crate::services::recovery::RecoveryResult::Recovered {
774 content,
775 ..
776 }) => {
777 if let Some(state) = self.buffers.get_mut(&buffer_id) {
779 let current_len = state.buffer.total_bytes();
780 let text = String::from_utf8_lossy(&content).into_owned();
781 let current =
782 state.buffer.get_text_range_mut(0, current_len).ok();
783 let current_text = current
784 .as_ref()
785 .map(|b| String::from_utf8_lossy(b).into_owned());
786 if current_text.as_deref() != Some(&text) {
787 state.buffer.delete(0..current_len);
788 state.buffer.insert(0, &text);
789 state.buffer.set_modified(true);
790 state.buffer.set_recovery_pending(false);
791 tracing::info!(
792 "Restored unsaved changes for {:?} from hot exit recovery",
793 file_path
794 );
795 }
796 }
797 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
800 log.clear_saved_position();
801 }
802 }
803 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
804 chunks,
805 ..
806 }) => {
807 if let Some(state) = self.buffers.get_mut(&buffer_id) {
809 for chunk in chunks.into_iter().rev() {
810 let text =
811 String::from_utf8_lossy(&chunk.content).into_owned();
812 if chunk.original_len > 0 {
813 state.buffer.delete(
814 chunk.offset..chunk.offset + chunk.original_len,
815 );
816 }
817 state.buffer.insert(chunk.offset, &text);
818 }
819 state.buffer.set_modified(true);
820 state.buffer.set_recovery_pending(false);
821 tracing::info!(
822 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
823 file_path
824 );
825 }
826 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
829 log.clear_saved_position();
830 }
831 }
832 Ok(
833 crate::services::recovery::RecoveryResult::OriginalFileModified {
834 original_path,
835 ..
836 },
837 ) => {
838 let name = original_path
839 .file_name()
840 .unwrap_or_default()
841 .to_string_lossy();
842 tracing::warn!(
843 "{} changed on disk; unsaved changes not restored",
844 name
845 );
846 self.set_status_message(format!(
847 "{} changed on disk; unsaved changes not restored",
848 name
849 ));
850 }
851 Ok(_) => {} Err(e) => {
853 tracing::debug!(
854 "Failed to load hot exit recovery for {:?}: {}",
855 file_path,
856 e
857 );
858 }
859 }
860 }
861 }
862 }
863 }
864
865 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
867 if self.config.editor.hot_exit && !workspace.unnamed_buffers.is_empty() {
868 tracing::debug!(
869 "Restoring {} unnamed buffers from recovery",
870 workspace.unnamed_buffers.len()
871 );
872 for unnamed_ref in &workspace.unnamed_buffers {
873 let entries = match self.recovery_service.list_recoverable() {
875 Ok(e) => e,
876 Err(e) => {
877 tracing::warn!("Failed to list recovery entries: {}", e);
878 continue;
879 }
880 };
881
882 let entry = entries.iter().find(|e| e.id == unnamed_ref.recovery_id);
883 if let Some(entry) = entry {
884 match self.recovery_service.load_recovery(entry) {
885 Ok(crate::services::recovery::RecoveryResult::Recovered {
886 content,
887 ..
888 }) => {
889 let text = String::from_utf8_lossy(&content).into_owned();
890 let buffer_id = self.new_buffer();
891 {
892 let state = self.active_state_mut();
893 state.buffer.insert(0, &text);
894 state.buffer.set_modified(true);
896 state.buffer.set_recovery_pending(false);
897 }
898 self.active_event_log_mut().clear_saved_position();
901
902 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
904 meta.recovery_id = Some(unnamed_ref.recovery_id.clone());
905 meta.display_name = unnamed_ref.display_name.clone();
906 }
907
908 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
909 tracing::info!(
910 "Restored unnamed buffer '{}' (recovery_id={})",
911 unnamed_ref.display_name,
912 unnamed_ref.recovery_id
913 );
914 }
915 Ok(other) => {
916 tracing::warn!(
917 "Unexpected recovery result for unnamed buffer {}: {:?}",
918 unnamed_ref.recovery_id,
919 std::mem::discriminant(&other)
920 );
921 }
922 Err(e) => {
923 tracing::warn!(
924 "Failed to load recovery for unnamed buffer {}: {}",
925 unnamed_ref.recovery_id,
926 e
927 );
928 }
929 }
930 } else {
931 tracing::debug!(
932 "Recovery file not found for unnamed buffer {}",
933 unnamed_ref.recovery_id
934 );
935 }
936 }
937 }
938
939 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
941 if !workspace.terminals.is_empty() {
942 if let Some(ref bridge) = self.async_bridge {
943 self.terminal_manager.set_async_bridge(bridge.clone());
944 }
945 for terminal in &workspace.terminals {
946 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
947 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
948 }
949 }
950 }
951
952 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
955 self.restore_split_node(
956 &workspace.split_layout,
957 &path_to_buffer,
958 &terminal_buffer_map,
959 &unnamed_buffer_map,
960 &workspace.split_states,
961 &mut split_id_map,
962 true, );
964
965 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
969 self.split_manager
970 .set_active_split(LeafId(new_active_split));
971 }
972
973 for (key, bookmark) in &workspace.bookmarks {
975 if let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) {
976 if let Some(buffer) = self.buffers.get(&buffer_id) {
978 let pos = bookmark.position.min(buffer.buffer.len());
979 self.bookmarks.insert(
980 *key,
981 Bookmark {
982 buffer_id,
983 position: pos,
984 },
985 );
986 }
987 }
988 }
989
990 let referenced: HashSet<BufferId> = self
993 .split_view_states
994 .values()
995 .flat_map(|vs| vs.open_buffers.iter().copied())
996 .collect();
997 let orphans: Vec<BufferId> =
998 self.buffers
999 .keys()
1000 .copied()
1001 .filter(|id| {
1002 !referenced.contains(id)
1003 && self.buffers.get(id).is_some_and(|s| {
1004 s.buffer.file_path().is_none() && !s.buffer.is_modified()
1005 })
1006 })
1007 .collect();
1008 for id in orphans {
1009 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
1010 self.buffers.remove(&id);
1011 self.event_logs.remove(&id);
1012 self.buffer_metadata.remove(&id);
1013 }
1014
1015 let restored_count = self
1017 .buffers
1018 .keys()
1019 .filter(|id| {
1020 self.buffer_metadata
1021 .get(id)
1022 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
1023 })
1024 .count();
1025 if restored_count > 0 {
1026 let session_label = self
1027 .session_name
1028 .as_ref()
1029 .map(|n| format!("session '{}'", n));
1030 let msg = if let Some(label) = session_label {
1031 format!("Restored {} ({} buffer(s))", label, restored_count)
1032 } else {
1033 format!(
1034 "Restored {} buffer(s) from previous session",
1035 restored_count
1036 )
1037 };
1038 self.set_status_message(msg);
1039 }
1040
1041 tracing::debug!(
1042 "Workspace restore complete: {} splits, {} buffers",
1043 self.split_view_states.len(),
1044 self.buffers.len()
1045 );
1046
1047 #[cfg(feature = "plugins")]
1052 {
1053 let buffer_id = self.active_buffer();
1054 self.update_plugin_state_snapshot();
1055 tracing::debug!(
1056 "Firing buffer_activated for active buffer {:?} after workspace restore",
1057 buffer_id
1058 );
1059 self.plugin_manager.run_hook(
1060 "buffer_activated",
1061 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
1062 );
1063 }
1064
1065 Ok(())
1066 }
1067
1068 fn restore_terminal_from_workspace(
1077 &mut self,
1078 terminal: &SerializedTerminalWorkspace,
1079 ) -> Option<BufferId> {
1080 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
1082 let log_path = if terminal.log_path.is_absolute() {
1083 terminal.log_path.clone()
1084 } else {
1085 terminals_root.join(&terminal.log_path)
1086 };
1087 let backing_path = if terminal.backing_path.is_absolute() {
1088 terminal.backing_path.clone()
1089 } else {
1090 terminals_root.join(&terminal.backing_path)
1091 };
1092
1093 #[allow(clippy::let_underscore_must_use)]
1095 let _ = self.filesystem.create_dir_all(
1096 log_path
1097 .parent()
1098 .or_else(|| backing_path.parent())
1099 .unwrap_or(&terminals_root),
1100 );
1101
1102 let predicted_id = self.terminal_manager.next_terminal_id();
1104 self.terminal_log_files
1105 .insert(predicted_id, log_path.clone());
1106 self.terminal_backing_files
1107 .insert(predicted_id, backing_path.clone());
1108
1109 let terminal_id = match self.terminal_manager.spawn(
1111 terminal.cols,
1112 terminal.rows,
1113 terminal.cwd.clone(),
1114 Some(log_path.clone()),
1115 Some(backing_path.clone()),
1116 ) {
1117 Ok(id) => id,
1118 Err(e) => {
1119 tracing::warn!(
1120 "Failed to restore terminal {}: {}",
1121 terminal.terminal_index,
1122 e
1123 );
1124 return None;
1125 }
1126 };
1127
1128 if terminal_id != predicted_id {
1130 self.terminal_log_files
1131 .insert(terminal_id, log_path.clone());
1132 self.terminal_backing_files
1133 .insert(terminal_id, backing_path.clone());
1134 self.terminal_log_files.remove(&predicted_id);
1135 self.terminal_backing_files.remove(&predicted_id);
1136 }
1137
1138 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1140
1141 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
1144
1145 Some(buffer_id)
1146 }
1147
1148 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
1153 if !backing_path.exists() {
1155 return;
1156 }
1157
1158 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1159 if let Ok(new_state) = EditorState::from_file_with_languages(
1160 backing_path,
1161 self.terminal_width,
1162 self.terminal_height,
1163 large_file_threshold,
1164 &self.grammar_registry,
1165 &self.config.languages,
1166 std::sync::Arc::clone(&self.filesystem),
1167 ) {
1168 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1169 *state = new_state;
1170 let total = state.buffer.total_bytes();
1172 for vs in self.split_view_states.values_mut() {
1174 if vs.open_buffers.contains(&buffer_id) {
1175 vs.cursors.primary_mut().position = total;
1176 }
1177 }
1178 state.buffer.set_modified(false);
1180 state.editing_disabled = true;
1182 state.margins.configure_for_line_numbers(false);
1183 }
1184 }
1185 }
1186
1187 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
1189 for (buffer_id, metadata) in &self.buffer_metadata {
1191 if let Some(file_path) = metadata.file_path() {
1192 if file_path == path {
1193 return Ok(*buffer_id);
1194 }
1195 }
1196 }
1197
1198 self.open_file(path).map_err(WorkspaceError::Io)
1200 }
1201
1202 #[allow(clippy::too_many_arguments)]
1204 fn restore_split_node(
1205 &mut self,
1206 node: &SerializedSplitNode,
1207 path_to_buffer: &HashMap<PathBuf, BufferId>,
1208 terminal_buffers: &HashMap<usize, BufferId>,
1209 unnamed_buffers: &HashMap<String, BufferId>,
1210 split_states: &HashMap<usize, SerializedSplitViewState>,
1211 split_id_map: &mut HashMap<usize, SplitId>,
1212 is_first_leaf: bool,
1213 ) {
1214 match node {
1215 SerializedSplitNode::Leaf {
1216 file_path,
1217 split_id,
1218 label,
1219 unnamed_recovery_id,
1220 } => {
1221 let buffer_id = file_path
1223 .as_ref()
1224 .and_then(|p| path_to_buffer.get(p).copied())
1225 .or_else(|| {
1226 unnamed_recovery_id
1227 .as_ref()
1228 .and_then(|id| unnamed_buffers.get(id).copied())
1229 })
1230 .unwrap_or(self.active_buffer());
1231
1232 let current_leaf_id = if is_first_leaf {
1233 let leaf_id = self.split_manager.active_split();
1235 self.split_manager.set_split_buffer(leaf_id, buffer_id);
1236 leaf_id
1237 } else {
1238 self.split_manager.active_split()
1240 };
1241
1242 split_id_map.insert(*split_id, current_leaf_id.into());
1244
1245 if let Some(label) = label {
1247 self.split_manager.set_label(current_leaf_id, label.clone());
1248 }
1249
1250 self.restore_split_view_state(
1252 current_leaf_id,
1253 *split_id,
1254 split_states,
1255 path_to_buffer,
1256 terminal_buffers,
1257 unnamed_buffers,
1258 );
1259 }
1260 SerializedSplitNode::Terminal {
1261 terminal_index,
1262 split_id,
1263 label,
1264 } => {
1265 let buffer_id = terminal_buffers
1266 .get(terminal_index)
1267 .copied()
1268 .unwrap_or(self.active_buffer());
1269
1270 let current_leaf_id = if is_first_leaf {
1271 let leaf_id = self.split_manager.active_split();
1272 self.split_manager.set_split_buffer(leaf_id, buffer_id);
1273 leaf_id
1274 } else {
1275 self.split_manager.active_split()
1276 };
1277
1278 split_id_map.insert(*split_id, current_leaf_id.into());
1279
1280 if let Some(label) = label {
1282 self.split_manager.set_label(current_leaf_id, label.clone());
1283 }
1284
1285 self.split_manager
1286 .set_split_buffer(current_leaf_id, buffer_id);
1287
1288 self.restore_split_view_state(
1289 current_leaf_id,
1290 *split_id,
1291 split_states,
1292 path_to_buffer,
1293 terminal_buffers,
1294 unnamed_buffers,
1295 );
1296 }
1297 SerializedSplitNode::Split {
1298 direction,
1299 first,
1300 second,
1301 ratio,
1302 split_id,
1303 } => {
1304 self.restore_split_node(
1306 first,
1307 path_to_buffer,
1308 terminal_buffers,
1309 unnamed_buffers,
1310 split_states,
1311 split_id_map,
1312 is_first_leaf,
1313 );
1314
1315 let second_buffer_id = get_first_leaf_buffer(
1317 second,
1318 path_to_buffer,
1319 terminal_buffers,
1320 unnamed_buffers,
1321 )
1322 .unwrap_or(self.active_buffer());
1323
1324 let split_direction = match direction {
1326 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1327 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1328 };
1329
1330 match self
1332 .split_manager
1333 .split_active(split_direction, second_buffer_id, *ratio)
1334 {
1335 Ok(new_leaf_id) => {
1336 let mut view_state = SplitViewState::with_buffer(
1338 self.terminal_width,
1339 self.terminal_height,
1340 second_buffer_id,
1341 );
1342 view_state.apply_config_defaults(
1343 self.config.editor.line_numbers,
1344 self.config.editor.highlight_current_line,
1345 self.resolve_line_wrap_for_buffer(second_buffer_id),
1346 self.config.editor.wrap_indent,
1347 self.resolve_wrap_column_for_buffer(second_buffer_id),
1348 self.config.editor.rulers.clone(),
1349 );
1350 self.split_view_states.insert(new_leaf_id, view_state);
1351
1352 split_id_map.insert(*split_id, new_leaf_id.into());
1354
1355 self.restore_split_node(
1357 second,
1358 path_to_buffer,
1359 terminal_buffers,
1360 unnamed_buffers,
1361 split_states,
1362 split_id_map,
1363 false,
1364 );
1365 }
1366 Err(e) => {
1367 tracing::error!("Failed to create split during workspace restore: {}", e);
1368 }
1369 }
1370 }
1371 }
1372 }
1373
1374 fn restore_split_view_state(
1376 &mut self,
1377 current_split_id: LeafId,
1378 saved_split_id: usize,
1379 split_states: &HashMap<usize, SerializedSplitViewState>,
1380 path_to_buffer: &HashMap<PathBuf, BufferId>,
1381 terminal_buffers: &HashMap<usize, BufferId>,
1382 unnamed_buffers: &HashMap<String, BufferId>,
1383 ) {
1384 let Some(split_state) = split_states.get(&saved_split_id) else {
1386 return;
1387 };
1388
1389 let Some(view_state) = self.split_view_states.get_mut(¤t_split_id) else {
1390 return;
1391 };
1392
1393 let mut active_buffer_id: Option<BufferId> = None;
1394
1395 if !split_state.open_tabs.is_empty() {
1396 view_state.open_buffers.clear();
1399
1400 for tab in &split_state.open_tabs {
1401 match tab {
1402 SerializedTabRef::File(rel_path) => {
1403 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1404 if !view_state.open_buffers.contains(&buffer_id) {
1405 view_state.open_buffers.push(buffer_id);
1406 }
1407 view_state.ensure_buffer_state(buffer_id);
1409 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1410 view_state
1411 .buffer_state_mut(buffer_id)
1412 .unwrap()
1413 .viewport
1414 .line_wrap_enabled = false;
1415 }
1416 }
1417 }
1418 SerializedTabRef::Terminal(index) => {
1419 if let Some(&buffer_id) = terminal_buffers.get(index) {
1420 if !view_state.open_buffers.contains(&buffer_id) {
1421 view_state.open_buffers.push(buffer_id);
1422 }
1423 view_state
1424 .ensure_buffer_state(buffer_id)
1425 .viewport
1426 .line_wrap_enabled = false;
1427 }
1428 }
1429 SerializedTabRef::Unnamed(recovery_id) => {
1430 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1431 if !view_state.open_buffers.contains(&buffer_id) {
1432 view_state.open_buffers.push(buffer_id);
1433 }
1434 view_state.ensure_buffer_state(buffer_id);
1435 }
1436 }
1437 }
1438 }
1439
1440 if view_state.open_buffers.is_empty() {
1445 if let Some(buf) = self.split_manager.buffer_for_split(current_split_id) {
1446 view_state.open_buffers.push(buf);
1447 view_state.ensure_buffer_state(buf);
1448 }
1449 }
1450
1451 if let Some(active_idx) = split_state.active_tab_index {
1452 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1453 active_buffer_id = match tab {
1454 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1455 SerializedTabRef::Terminal(index) => terminal_buffers.get(index).copied(),
1456 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1457 };
1458 }
1459 }
1460 } else {
1461 for rel_path in &split_state.open_files {
1463 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1464 if !view_state.open_buffers.contains(&buffer_id) {
1465 view_state.open_buffers.push(buffer_id);
1466 }
1467 view_state.ensure_buffer_state(buffer_id);
1468 }
1469 }
1470
1471 let active_file_path = split_state.open_files.get(split_state.active_file_index);
1472 active_buffer_id =
1473 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1474 }
1475
1476 for (rel_path, file_state) in &split_state.file_states {
1478 let rel_str = rel_path.to_string_lossy();
1480 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1481 match unnamed_buffers.get(recovery_id).copied() {
1482 Some(id) => id,
1483 None => continue,
1484 }
1485 } else {
1486 match path_to_buffer.get(rel_path).copied() {
1487 Some(id) => id,
1488 None => continue,
1489 }
1490 };
1491 let max_pos = self
1492 .buffers
1493 .get(&buffer_id)
1494 .map(|b| b.buffer.len())
1495 .unwrap_or(0);
1496
1497 let buf_state = view_state.ensure_buffer_state(buffer_id);
1499
1500 let cursor_pos = file_state.cursor.position.min(max_pos);
1501 buf_state.cursors.primary_mut().position = cursor_pos;
1502 buf_state.cursors.primary_mut().anchor =
1503 file_state.cursor.anchor.map(|a| a.min(max_pos));
1504 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1505
1506 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1507 buf_state.viewport.top_view_line_offset = file_state.scroll.top_view_line_offset;
1508 buf_state.viewport.left_column = file_state.scroll.left_column;
1509 buf_state.viewport.set_skip_resize_sync();
1510
1511 buf_state.view_mode = match file_state.view_mode {
1513 SerializedViewMode::Source => ViewMode::Source,
1514 SerializedViewMode::PageView => ViewMode::PageView,
1515 };
1516 buf_state.compose_width = file_state.compose_width;
1517 buf_state.plugin_state = file_state.plugin_state.clone();
1518 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1519 buf_state.folds.clear(&mut state.marker_list);
1520 for fold in &file_state.folds {
1521 let start_line = fold.header_line.saturating_add(1);
1522 let end_line = fold.end_line;
1523 if start_line > end_line {
1524 continue;
1525 }
1526 let Some(start_byte) = state.buffer.line_start_offset(start_line) else {
1527 continue;
1528 };
1529 let end_byte = state
1530 .buffer
1531 .line_start_offset(end_line.saturating_add(1))
1532 .unwrap_or_else(|| state.buffer.len());
1533 buf_state.folds.add(
1534 &mut state.marker_list,
1535 start_byte,
1536 end_byte,
1537 fold.placeholder.clone(),
1538 );
1539 }
1540 }
1541
1542 tracing::trace!(
1543 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1544 rel_path,
1545 cursor_pos,
1546 buf_state.viewport.top_byte,
1547 buf_state.view_mode,
1548 );
1549 }
1550
1551 let restored_view_mode = match split_state.view_mode {
1554 SerializedViewMode::Source => ViewMode::Source,
1555 SerializedViewMode::PageView => ViewMode::PageView,
1556 };
1557
1558 if let Some(active_id) = active_buffer_id {
1559 view_state.switch_buffer(active_id);
1561
1562 let active_has_file_state = split_state
1564 .file_states
1565 .keys()
1566 .any(|rel_path| path_to_buffer.get(rel_path).copied() == Some(active_id));
1567 if !active_has_file_state {
1568 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1569 view_state.active_state_mut().compose_width = split_state.compose_width;
1570 }
1571
1572 self.split_manager
1576 .set_split_buffer(current_split_id, active_id);
1577 }
1578 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1579 }
1580}
1581
1582fn get_first_leaf_buffer(
1584 node: &SerializedSplitNode,
1585 path_to_buffer: &HashMap<PathBuf, BufferId>,
1586 terminal_buffers: &HashMap<usize, BufferId>,
1587 unnamed_buffers: &HashMap<String, BufferId>,
1588) -> Option<BufferId> {
1589 match node {
1590 SerializedSplitNode::Leaf {
1591 file_path,
1592 unnamed_recovery_id,
1593 ..
1594 } => file_path
1595 .as_ref()
1596 .and_then(|p| path_to_buffer.get(p).copied())
1597 .or_else(|| {
1598 unnamed_recovery_id
1599 .as_ref()
1600 .and_then(|id| unnamed_buffers.get(id).copied())
1601 }),
1602 SerializedSplitNode::Terminal { terminal_index, .. } => {
1603 terminal_buffers.get(terminal_index).copied()
1604 }
1605 SerializedSplitNode::Split { first, .. } => {
1606 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
1607 }
1608 }
1609}
1610
1611fn serialize_split_node(
1616 node: &SplitNode,
1617 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1618 working_dir: &Path,
1619 terminal_buffers: &HashMap<BufferId, TerminalId>,
1620 terminal_indices: &HashMap<TerminalId, usize>,
1621 split_labels: &HashMap<SplitId, String>,
1622) -> SerializedSplitNode {
1623 match node {
1624 SplitNode::Leaf {
1625 buffer_id,
1626 split_id,
1627 } => {
1628 let raw_split_id: SplitId = (*split_id).into();
1629 let label = split_labels.get(&raw_split_id).cloned();
1630
1631 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1632 if let Some(index) = terminal_indices.get(terminal_id) {
1633 return SerializedSplitNode::Terminal {
1634 terminal_index: *index,
1635 split_id: raw_split_id.0,
1636 label,
1637 };
1638 }
1639 }
1640
1641 let meta = buffer_metadata.get(buffer_id);
1642 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
1643 if abs_path.as_os_str().is_empty() {
1644 None } else {
1646 abs_path
1647 .strip_prefix(working_dir)
1648 .ok()
1649 .map(|p| p.to_path_buf())
1650 }
1651 });
1652
1653 let unnamed_recovery_id = if file_path.is_none() {
1656 meta.and_then(|m| m.recovery_id.clone())
1657 } else {
1658 None
1659 };
1660
1661 SerializedSplitNode::Leaf {
1662 file_path,
1663 split_id: raw_split_id.0,
1664 label,
1665 unnamed_recovery_id,
1666 }
1667 }
1668 SplitNode::Split {
1669 direction,
1670 first,
1671 second,
1672 ratio,
1673 split_id,
1674 } => {
1675 let raw_split_id: SplitId = (*split_id).into();
1676 SerializedSplitNode::Split {
1677 direction: match direction {
1678 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
1679 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
1680 },
1681 first: Box::new(serialize_split_node(
1682 first,
1683 buffer_metadata,
1684 working_dir,
1685 terminal_buffers,
1686 terminal_indices,
1687 split_labels,
1688 )),
1689 second: Box::new(serialize_split_node(
1690 second,
1691 buffer_metadata,
1692 working_dir,
1693 terminal_buffers,
1694 terminal_indices,
1695 split_labels,
1696 )),
1697 ratio: *ratio,
1698 split_id: raw_split_id.0,
1699 }
1700 }
1701 }
1702}
1703
1704fn serialize_split_view_state(
1705 view_state: &crate::view::split::SplitViewState,
1706 buffers: &HashMap<BufferId, EditorState>,
1707 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1708 working_dir: &Path,
1709 active_buffer: Option<BufferId>,
1710 terminal_buffers: &HashMap<BufferId, TerminalId>,
1711 terminal_indices: &HashMap<TerminalId, usize>,
1712) -> SerializedSplitViewState {
1713 let mut open_tabs = Vec::new();
1714 let mut open_files = Vec::new();
1715 let mut active_tab_index = None;
1716
1717 for buffer_id in &view_state.open_buffers {
1718 let tab_index = open_tabs.len();
1719 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1720 if let Some(idx) = terminal_indices.get(terminal_id) {
1721 open_tabs.push(SerializedTabRef::Terminal(*idx));
1722 if Some(*buffer_id) == active_buffer {
1723 active_tab_index = Some(tab_index);
1724 }
1725 continue;
1726 }
1727 }
1728
1729 if let Some(meta) = buffer_metadata.get(buffer_id) {
1730 if let Some(abs_path) = meta.file_path() {
1731 if abs_path.as_os_str().is_empty() {
1732 if let Some(ref recovery_id) = meta.recovery_id {
1734 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
1735 if Some(*buffer_id) == active_buffer {
1736 active_tab_index = Some(tab_index);
1737 }
1738 }
1739 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
1740 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
1741 open_files.push(rel_path.to_path_buf());
1742 if Some(*buffer_id) == active_buffer {
1743 active_tab_index = Some(tab_index);
1744 }
1745 } else {
1746 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
1748 if Some(*buffer_id) == active_buffer {
1749 active_tab_index = Some(tab_index);
1750 }
1751 }
1752 }
1753 }
1754 }
1755
1756 let active_file_index = active_tab_index
1758 .and_then(|idx| open_tabs.get(idx))
1759 .and_then(|tab| match tab {
1760 SerializedTabRef::File(path) => {
1761 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
1762 }
1763 _ => None,
1764 })
1765 .unwrap_or(0);
1766
1767 let mut file_states = HashMap::new();
1769 for (buffer_id, buf_state) in &view_state.keyed_states {
1770 let Some(meta) = buffer_metadata.get(buffer_id) else {
1771 continue;
1772 };
1773 let Some(abs_path) = meta.file_path() else {
1774 continue;
1775 };
1776
1777 let state_key = if abs_path.as_os_str().is_empty() {
1779 if let Some(ref recovery_id) = meta.recovery_id {
1781 PathBuf::from(format!("__unnamed__{}", recovery_id))
1782 } else {
1783 continue;
1784 }
1785 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
1786 rp.to_path_buf()
1787 } else {
1788 abs_path.to_path_buf()
1790 };
1791
1792 let primary_cursor = buf_state.cursors.primary();
1793 let folds = buffers
1794 .get(buffer_id)
1795 .map(|state| {
1796 buf_state
1797 .folds
1798 .collapsed_line_ranges(&state.buffer, &state.marker_list)
1799 .into_iter()
1800 .map(|range| SerializedFoldRange {
1801 header_line: range.header_line,
1802 end_line: range.end_line,
1803 placeholder: range.placeholder,
1804 })
1805 .collect::<Vec<_>>()
1806 })
1807 .unwrap_or_default();
1808
1809 file_states.insert(
1810 state_key,
1811 SerializedFileState {
1812 cursor: SerializedCursor {
1813 position: primary_cursor.position,
1814 anchor: primary_cursor.anchor,
1815 sticky_column: primary_cursor.sticky_column,
1816 },
1817 additional_cursors: buf_state
1818 .cursors
1819 .iter()
1820 .skip(1) .map(|(_, cursor)| SerializedCursor {
1822 position: cursor.position,
1823 anchor: cursor.anchor,
1824 sticky_column: cursor.sticky_column,
1825 })
1826 .collect(),
1827 scroll: SerializedScroll {
1828 top_byte: buf_state.viewport.top_byte,
1829 top_view_line_offset: buf_state.viewport.top_view_line_offset,
1830 left_column: buf_state.viewport.left_column,
1831 },
1832 view_mode: match buf_state.view_mode {
1833 ViewMode::Source => SerializedViewMode::Source,
1834 ViewMode::PageView => SerializedViewMode::PageView,
1835 },
1836 compose_width: buf_state.compose_width,
1837 plugin_state: buf_state.plugin_state.clone(),
1838 folds,
1839 },
1840 );
1841 }
1842
1843 let active_view_mode = active_buffer
1845 .and_then(|id| view_state.keyed_states.get(&id))
1846 .map(|bs| match bs.view_mode {
1847 ViewMode::Source => SerializedViewMode::Source,
1848 ViewMode::PageView => SerializedViewMode::PageView,
1849 })
1850 .unwrap_or(SerializedViewMode::Source);
1851 let active_compose_width = active_buffer
1852 .and_then(|id| view_state.keyed_states.get(&id))
1853 .and_then(|bs| bs.compose_width);
1854
1855 SerializedSplitViewState {
1856 open_tabs,
1857 active_tab_index,
1858 open_files,
1859 active_file_index,
1860 file_states,
1861 tab_scroll_offset: view_state.tab_scroll_offset,
1862 view_mode: active_view_mode,
1863 compose_width: active_compose_width,
1864 }
1865}
1866
1867fn serialize_bookmarks(
1868 bookmarks: &HashMap<char, Bookmark>,
1869 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1870 working_dir: &Path,
1871) -> HashMap<char, SerializedBookmark> {
1872 bookmarks
1873 .iter()
1874 .filter_map(|(key, bookmark)| {
1875 buffer_metadata
1876 .get(&bookmark.buffer_id)
1877 .and_then(|meta| meta.file_path())
1878 .and_then(|abs_path| {
1879 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
1880 (
1881 *key,
1882 SerializedBookmark {
1883 file_path: rel_path.to_path_buf(),
1884 position: bookmark.position,
1885 },
1886 )
1887 })
1888 })
1889 })
1890 .collect()
1891}
1892
1893fn collect_file_paths_from_states(
1895 split_states: &HashMap<usize, SerializedSplitViewState>,
1896) -> Vec<PathBuf> {
1897 let mut paths = Vec::new();
1898 for state in split_states.values() {
1899 if !state.open_tabs.is_empty() {
1900 for tab in &state.open_tabs {
1901 if let SerializedTabRef::File(path) = tab {
1902 if !paths.contains(path) {
1903 paths.push(path.clone());
1904 }
1905 }
1906 }
1907 } else {
1908 for path in &state.open_files {
1909 if !paths.contains(path) {
1910 paths.push(path.clone());
1911 }
1912 }
1913 }
1914 }
1915 paths
1916}
1917
1918fn get_expanded_dirs(
1920 explorer: &crate::view::file_tree::FileTreeView,
1921 working_dir: &Path,
1922) -> Vec<PathBuf> {
1923 let mut expanded = Vec::new();
1924 let tree = explorer.tree();
1925
1926 for node in tree.all_nodes() {
1928 if node.is_expanded() && node.is_dir() {
1929 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
1931 expanded.push(rel_path.to_path_buf());
1932 }
1933 }
1934 }
1935
1936 expanded
1937}