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.terminal_buffers.values().copied() {
160 if seen.insert(terminal_id) {
161 let idx = terminals.len();
162 terminal_indices.insert(terminal_id, idx);
163 let handle = self.terminal_manager.get(terminal_id);
164 let (cols, rows) = handle
165 .map(|h| h.size())
166 .unwrap_or((self.terminal_width, self.terminal_height));
167 let cwd = handle.and_then(|h| h.cwd());
168 let shell = handle
169 .map(|h| h.shell().to_string())
170 .unwrap_or_else(crate::services::terminal::detect_shell);
171 let log_path = self
172 .terminal_log_files
173 .get(&terminal_id)
174 .cloned()
175 .unwrap_or_else(|| {
176 let root = self.dir_context.terminal_dir_for(&self.working_dir);
177 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
178 });
179 let backing_path = self
180 .terminal_backing_files
181 .get(&terminal_id)
182 .cloned()
183 .unwrap_or_else(|| {
184 let root = self.dir_context.terminal_dir_for(&self.working_dir);
185 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
186 });
187
188 terminals.push(SerializedTerminalWorkspace {
189 terminal_index: idx,
190 cwd,
191 shell,
192 cols,
193 rows,
194 log_path,
195 backing_path,
196 });
197 }
198 }
199
200 let split_layout = serialize_split_node(
201 self.split_manager.root(),
202 &self.buffer_metadata,
203 &self.working_dir,
204 &self.terminal_buffers,
205 &terminal_indices,
206 self.split_manager.labels(),
207 );
208
209 let active_buffers: HashMap<LeafId, BufferId> = self
212 .split_manager
213 .root()
214 .get_leaves_with_rects(ratatui::layout::Rect::default())
215 .into_iter()
216 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
217 .collect();
218
219 let mut split_states = HashMap::new();
220 for (leaf_id, view_state) in &self.split_view_states {
221 let active_buffer = active_buffers.get(leaf_id).copied();
222 let serialized = serialize_split_view_state(
223 view_state,
224 &self.buffers,
225 &self.buffer_metadata,
226 &self.working_dir,
227 active_buffer,
228 &self.terminal_buffers,
229 &terminal_indices,
230 );
231 tracing::trace!(
232 "Split {:?}: {} open tabs, active_buffer={:?}",
233 leaf_id,
234 serialized.open_tabs.len(),
235 active_buffer
236 );
237 split_states.insert(leaf_id.0 .0, serialized);
238 }
239
240 tracing::debug!(
241 "Captured {} split states, active_split={}",
242 split_states.len(),
243 SplitId::from(self.split_manager.active_split()).0
244 );
245
246 let file_explorer = if let Some(ref explorer) = self.file_explorer {
248 let expanded_dirs = get_expanded_dirs(explorer, &self.working_dir);
250 FileExplorerState {
251 visible: self.file_explorer_visible,
252 width_percent: self.file_explorer_width_percent,
253 expanded_dirs,
254 scroll_offset: explorer.get_scroll_offset(),
255 show_hidden: explorer.ignore_patterns().show_hidden(),
256 show_gitignored: explorer.ignore_patterns().show_gitignored(),
257 }
258 } else {
259 FileExplorerState {
260 visible: self.file_explorer_visible,
261 width_percent: self.file_explorer_width_percent,
262 expanded_dirs: Vec::new(),
263 scroll_offset: 0,
264 show_hidden: false,
265 show_gitignored: false,
266 }
267 };
268
269 let config_overrides = WorkspaceConfigOverrides {
271 line_numbers: Some(self.config.editor.line_numbers),
272 relative_line_numbers: Some(self.config.editor.relative_line_numbers),
273 line_wrap: Some(self.config.editor.line_wrap),
274 syntax_highlighting: Some(self.config.editor.syntax_highlighting),
275 enable_inlay_hints: Some(self.config.editor.enable_inlay_hints),
276 mouse_enabled: Some(self.mouse_enabled),
277 menu_bar_hidden: Some(!self.menu_bar_visible),
278 };
279
280 let histories = WorkspaceHistories {
282 search: self
283 .prompt_histories
284 .get("search")
285 .map(|h| h.items().to_vec())
286 .unwrap_or_default(),
287 replace: self
288 .prompt_histories
289 .get("replace")
290 .map(|h| h.items().to_vec())
291 .unwrap_or_default(),
292 command_palette: Vec::new(), goto_line: self
294 .prompt_histories
295 .get("goto_line")
296 .map(|h| h.items().to_vec())
297 .unwrap_or_default(),
298 open_file: Vec::new(), };
300 tracing::trace!(
301 "Captured histories: {} search, {} replace",
302 histories.search.len(),
303 histories.replace.len()
304 );
305
306 let search_options = SearchOptions {
308 case_sensitive: self.search_case_sensitive,
309 whole_word: self.search_whole_word,
310 use_regex: self.search_use_regex,
311 confirm_each: self.search_confirm_each,
312 };
313
314 let bookmarks =
316 serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.working_dir);
317
318 let external_files: Vec<PathBuf> = self
321 .buffer_metadata
322 .values()
323 .filter_map(|meta| meta.file_path())
324 .filter(|abs_path| abs_path.strip_prefix(&self.working_dir).is_err())
325 .cloned()
326 .collect();
327 if !external_files.is_empty() {
328 tracing::debug!("Captured {} external files", external_files.len());
329 }
330
331 let read_only_files: Vec<PathBuf> = self
335 .buffer_metadata
336 .values()
337 .filter(|meta| meta.read_only)
338 .filter_map(|meta| meta.file_path().cloned())
339 .filter(|p| !p.as_os_str().is_empty())
340 .map(|p| {
341 p.strip_prefix(&self.working_dir)
342 .map(|rel| rel.to_path_buf())
343 .unwrap_or(p)
344 })
345 .collect();
346
347 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.config.editor.hot_exit {
349 self.buffer_metadata
350 .iter()
351 .filter_map(|(buffer_id, meta)| {
352 let path = meta.file_path()?;
354 if !path.as_os_str().is_empty() {
355 return None;
356 }
357 if meta.hidden_from_tabs || meta.is_virtual() {
359 return None;
360 }
361 let state = self.buffers.get(buffer_id)?;
363 if state.buffer.total_bytes() == 0 {
364 return None;
365 }
366 let recovery_id = meta.recovery_id.clone()?;
368 Some(UnnamedBufferRef {
369 recovery_id,
370 display_name: meta.display_name.clone(),
371 })
372 })
373 .collect()
374 } else {
375 Vec::new()
376 };
377 if !unnamed_buffers.is_empty() {
378 tracing::debug!("Captured {} unnamed buffers", unnamed_buffers.len());
379 }
380
381 Workspace {
382 version: WORKSPACE_VERSION,
383 working_dir: self.working_dir.clone(),
384 split_layout,
385 active_split_id: SplitId::from(self.split_manager.active_split()).0,
386 split_states,
387 config_overrides,
388 file_explorer,
389 histories,
390 search_options,
391 bookmarks,
392 terminals,
393 external_files,
394 read_only_files,
395 unnamed_buffers,
396 plugin_global_state: self.plugin_global_state.clone(),
397 saved_at: std::time::SystemTime::now()
398 .duration_since(std::time::UNIX_EPOCH)
399 .unwrap_or_default()
400 .as_secs(),
401 }
402 }
403
404 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
410 self.sync_all_terminal_backing_files();
412
413 self.save_all_global_file_states();
415
416 let workspace = self.capture_workspace();
417
418 if let Some(ref session_name) = self.session_name {
420 workspace.save_session(session_name)
421 } else {
422 workspace.save()
423 }
424 }
425
426 fn save_all_global_file_states(&self) {
428 for (leaf_id, view_state) in &self.split_view_states {
430 let active_buffer = self
432 .split_manager
433 .root()
434 .get_leaves_with_rects(ratatui::layout::Rect::default())
435 .into_iter()
436 .find(|(sid, _, _)| *sid == *leaf_id)
437 .map(|(_, buffer_id, _)| buffer_id);
438
439 if let Some(buffer_id) = active_buffer {
440 self.save_buffer_file_state(buffer_id, view_state);
441 }
442 }
443 }
444
445 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
447 let abs_path = match self.buffer_metadata.get(&buffer_id) {
449 Some(metadata) => match metadata.file_path() {
450 Some(path) => path.to_path_buf(),
451 None => return, },
453 None => return,
454 };
455
456 let primary_cursor = view_state.cursors.primary();
458 let file_state = SerializedFileState {
459 cursor: SerializedCursor {
460 position: primary_cursor.position,
461 anchor: primary_cursor.anchor,
462 sticky_column: primary_cursor.sticky_column,
463 },
464 additional_cursors: view_state
465 .cursors
466 .iter()
467 .skip(1)
468 .map(|(_, cursor)| SerializedCursor {
469 position: cursor.position,
470 anchor: cursor.anchor,
471 sticky_column: cursor.sticky_column,
472 })
473 .collect(),
474 scroll: SerializedScroll {
475 top_byte: view_state.viewport.top_byte,
476 top_view_line_offset: view_state.viewport.top_view_line_offset,
477 left_column: view_state.viewport.left_column,
478 },
479 view_mode: Default::default(),
480 compose_width: None,
481 plugin_state: std::collections::HashMap::new(),
482 folds: Vec::new(),
483 };
484
485 PersistedFileWorkspace::save(&abs_path, file_state);
487 }
488
489 fn sync_all_terminal_backing_files(&mut self) {
494 use std::io::BufWriter;
495
496 let terminals_to_sync: Vec<_> = self
498 .terminal_buffers
499 .values()
500 .copied()
501 .filter_map(|terminal_id| {
502 self.terminal_backing_files
503 .get(&terminal_id)
504 .map(|path| (terminal_id, path.clone()))
505 })
506 .collect();
507
508 for (terminal_id, backing_path) in terminals_to_sync {
509 if let Some(handle) = self.terminal_manager.get(terminal_id) {
510 if let Ok(state) = handle.state.lock() {
511 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_path) {
513 let mut writer = BufWriter::new(&mut *file);
514 if let Err(e) = state.append_visible_screen(&mut writer) {
515 tracing::warn!(
516 "Failed to sync terminal {:?} to backing file: {}",
517 terminal_id,
518 e
519 );
520 }
521 }
522 }
523 }
524 }
525 }
526
527 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
531 tracing::debug!("Attempting to restore workspace for {:?}", self.working_dir);
532
533 let workspace = if let Some(ref session_name) = self.session_name {
535 Workspace::load_session(session_name, &self.working_dir)?
536 } else {
537 Workspace::load(&self.working_dir)?
538 };
539
540 match workspace {
541 Some(workspace) => {
542 tracing::info!("Found workspace, applying...");
543 self.apply_workspace(&workspace)?;
544 Ok(true)
545 }
546 None => {
547 tracing::debug!("No workspace found for {:?}", self.working_dir);
548 Ok(false)
549 }
550 }
551 }
552
553 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
559 if !self.config.editor.hot_exit {
560 return Ok(0);
561 }
562
563 let entries = self.recovery_service.list_recoverable()?;
564 if entries.is_empty() {
565 return Ok(0);
566 }
567
568 let buffer_files: Vec<_> = self
570 .buffers
571 .iter()
572 .filter_map(|(buffer_id, state)| {
573 let path = state.buffer.file_path()?.to_path_buf();
574 if path.as_os_str().is_empty() {
575 return None; }
577 Some((*buffer_id, path))
578 })
579 .collect();
580
581 let mut recovered = 0;
582 for (buffer_id, file_path) in buffer_files {
583 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
584 let entry = entries.iter().find(|e| e.id == recovery_id);
585 if let Some(entry) = entry {
586 match self.recovery_service.load_recovery(entry) {
587 Ok(crate::services::recovery::RecoveryResult::Recovered {
588 content, ..
589 }) => {
590 if let Some(state) = self.buffers.get_mut(&buffer_id) {
591 let current_len = state.buffer.total_bytes();
592 let text = String::from_utf8_lossy(&content).into_owned();
593 let current = state.buffer.get_text_range_mut(0, current_len).ok();
594 let current_text = current
595 .as_ref()
596 .map(|b| String::from_utf8_lossy(b).into_owned());
597 if current_text.as_deref() != Some(&text) {
598 state.buffer.delete(0..current_len);
599 state.buffer.insert(0, &text);
600 state.buffer.set_modified(true);
601 state.buffer.set_recovery_pending(false);
602 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
605 log.clear_saved_position();
606 }
607 recovered += 1;
608 tracing::info!(
609 "Restored unsaved changes for {:?} from hot exit recovery",
610 file_path
611 );
612 }
613 }
614 }
615 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
616 chunks,
617 ..
618 }) => {
619 if let Some(state) = self.buffers.get_mut(&buffer_id) {
620 for chunk in chunks.into_iter().rev() {
621 let text = String::from_utf8_lossy(&chunk.content).into_owned();
622 if chunk.original_len > 0 {
623 state
624 .buffer
625 .delete(chunk.offset..chunk.offset + chunk.original_len);
626 }
627 state.buffer.insert(chunk.offset, &text);
628 }
629 state.buffer.set_modified(true);
630 state.buffer.set_recovery_pending(false);
631 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
634 log.clear_saved_position();
635 }
636 recovered += 1;
637 tracing::info!(
638 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
639 file_path
640 );
641 }
642 }
643 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
644 original_path,
645 ..
646 }) => {
647 let name = original_path
648 .file_name()
649 .unwrap_or_default()
650 .to_string_lossy();
651 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
652 self.set_status_message(format!(
653 "{} changed on disk; unsaved changes not restored",
654 name
655 ));
656 }
657 Ok(_) => {} Err(e) => {
659 tracing::debug!(
660 "Failed to load hot exit recovery for {:?}: {}",
661 file_path,
662 e
663 );
664 }
665 }
666 }
667 }
668
669 Ok(recovered)
670 }
671
672 pub fn apply_workspace(&mut self, workspace: &Workspace) -> Result<(), WorkspaceError> {
674 tracing::debug!(
675 "Applying workspace with {} split states",
676 workspace.split_states.len()
677 );
678
679 if let Some(line_numbers) = workspace.config_overrides.line_numbers {
681 self.config_mut().editor.line_numbers = line_numbers;
682 }
683 if let Some(relative_line_numbers) = workspace.config_overrides.relative_line_numbers {
684 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
685 }
686 if let Some(line_wrap) = workspace.config_overrides.line_wrap {
687 self.config_mut().editor.line_wrap = line_wrap;
688 }
689 if let Some(syntax_highlighting) = workspace.config_overrides.syntax_highlighting {
690 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
691 }
692 if let Some(enable_inlay_hints) = workspace.config_overrides.enable_inlay_hints {
693 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
694 }
695 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
696 self.mouse_enabled = mouse_enabled;
697 }
698 if let Some(menu_bar_hidden) = workspace.config_overrides.menu_bar_hidden {
699 self.menu_bar_visible = !menu_bar_hidden;
700 }
701
702 if !workspace.plugin_global_state.is_empty() {
704 tracing::debug!(
705 "Restoring plugin global state for {} plugins",
706 workspace.plugin_global_state.len()
707 );
708 self.plugin_global_state = workspace.plugin_global_state.clone();
709 }
710
711 self.search_case_sensitive = workspace.search_options.case_sensitive;
713 self.search_whole_word = workspace.search_options.whole_word;
714 self.search_use_regex = workspace.search_options.use_regex;
715 self.search_confirm_each = workspace.search_options.confirm_each;
716
717 tracing::debug!(
719 "Restoring histories: {} search, {} replace, {} goto_line",
720 workspace.histories.search.len(),
721 workspace.histories.replace.len(),
722 workspace.histories.goto_line.len()
723 );
724 for item in &workspace.histories.search {
725 self.get_or_create_prompt_history("search")
726 .push(item.clone());
727 }
728 for item in &workspace.histories.replace {
729 self.get_or_create_prompt_history("replace")
730 .push(item.clone());
731 }
732 for item in &workspace.histories.goto_line {
733 self.get_or_create_prompt_history("goto_line")
734 .push(item.clone());
735 }
736
737 self.file_explorer_visible = workspace.file_explorer.visible;
739 self.file_explorer_width_percent = workspace.file_explorer.width_percent;
740
741 if workspace.file_explorer.show_hidden {
744 self.pending_file_explorer_show_hidden = Some(true);
745 }
746 if workspace.file_explorer.show_gitignored {
747 self.pending_file_explorer_show_gitignored = Some(true);
748 }
749
750 if self.file_explorer_visible && self.file_explorer.is_none() {
753 self.init_file_explorer();
754 }
755
756 let file_paths = collect_file_paths_from_states(&workspace.split_states);
759 tracing::debug!(
760 "Workspace has {} files to restore: {:?}",
761 file_paths.len(),
762 file_paths
763 );
764 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
765
766 for rel_path in file_paths {
767 let abs_path = self.working_dir.join(&rel_path);
768 tracing::trace!(
769 "Checking file: {:?} (exists: {})",
770 abs_path,
771 abs_path.exists()
772 );
773 if abs_path.exists() {
774 match self.open_file_internal(&abs_path) {
776 Ok(buffer_id) => {
777 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
778 path_to_buffer.insert(rel_path, buffer_id);
779 }
780 Err(e) => {
781 tracing::warn!("Failed to open file {:?}: {}", abs_path, e);
782 }
783 }
784 } else {
785 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
786 }
787 }
788
789 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
790
791 if !workspace.external_files.is_empty() {
794 tracing::debug!(
795 "Restoring {} external files: {:?}",
796 workspace.external_files.len(),
797 workspace.external_files
798 );
799 for abs_path in &workspace.external_files {
800 if abs_path.exists() {
801 match self.open_file_internal(abs_path) {
802 Ok(buffer_id) => {
803 path_to_buffer.insert(abs_path.clone(), buffer_id);
805 tracing::debug!(
806 "Restored external file {:?} as buffer {:?}",
807 abs_path,
808 buffer_id
809 );
810 }
811 Err(e) => {
812 tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e);
813 }
814 }
815 } else {
816 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
817 }
818 }
819 }
820
821 for ro_path in &workspace.read_only_files {
825 let buffer_id = path_to_buffer
826 .get(ro_path)
827 .copied()
828 .or_else(|| path_to_buffer.get(&self.working_dir.join(ro_path)).copied());
829 if let Some(id) = buffer_id {
830 self.mark_buffer_read_only(id, true);
831 }
832 }
833
834 if self.config.editor.hot_exit {
836 let entries = self.recovery_service.list_recoverable().unwrap_or_default();
837 if !entries.is_empty() {
838 for &buffer_id in path_to_buffer.values() {
839 let file_path = self
840 .buffers
841 .get(&buffer_id)
842 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
843 let file_path = match file_path {
844 Some(p) => p,
845 None => continue,
846 };
847
848 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
850 let entry = entries.iter().find(|e| e.id == recovery_id);
851 if let Some(entry) = entry {
852 match self.recovery_service.load_recovery(entry) {
853 Ok(crate::services::recovery::RecoveryResult::Recovered {
854 content,
855 ..
856 }) => {
857 if let Some(state) = self.buffers.get_mut(&buffer_id) {
859 let current_len = state.buffer.total_bytes();
860 let text = String::from_utf8_lossy(&content).into_owned();
861 let current =
862 state.buffer.get_text_range_mut(0, current_len).ok();
863 let current_text = current
864 .as_ref()
865 .map(|b| String::from_utf8_lossy(b).into_owned());
866 if current_text.as_deref() != Some(&text) {
867 state.buffer.delete(0..current_len);
868 state.buffer.insert(0, &text);
869 state.buffer.set_modified(true);
870 state.buffer.set_recovery_pending(false);
871 tracing::info!(
872 "Restored unsaved changes for {:?} from hot exit recovery",
873 file_path
874 );
875 }
876 }
877 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
880 log.clear_saved_position();
881 }
882 }
883 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
884 chunks,
885 ..
886 }) => {
887 if let Some(state) = self.buffers.get_mut(&buffer_id) {
889 for chunk in chunks.into_iter().rev() {
890 let text =
891 String::from_utf8_lossy(&chunk.content).into_owned();
892 if chunk.original_len > 0 {
893 state.buffer.delete(
894 chunk.offset..chunk.offset + chunk.original_len,
895 );
896 }
897 state.buffer.insert(chunk.offset, &text);
898 }
899 state.buffer.set_modified(true);
900 state.buffer.set_recovery_pending(false);
901 tracing::info!(
902 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
903 file_path
904 );
905 }
906 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
909 log.clear_saved_position();
910 }
911 }
912 Ok(
913 crate::services::recovery::RecoveryResult::OriginalFileModified {
914 original_path,
915 ..
916 },
917 ) => {
918 let name = original_path
919 .file_name()
920 .unwrap_or_default()
921 .to_string_lossy();
922 tracing::warn!(
923 "{} changed on disk; unsaved changes not restored",
924 name
925 );
926 self.set_status_message(format!(
927 "{} changed on disk; unsaved changes not restored",
928 name
929 ));
930 }
931 Ok(_) => {} Err(e) => {
933 tracing::debug!(
934 "Failed to load hot exit recovery for {:?}: {}",
935 file_path,
936 e
937 );
938 }
939 }
940 }
941 }
942 }
943 }
944
945 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
947 if self.config.editor.hot_exit && !workspace.unnamed_buffers.is_empty() {
948 tracing::debug!(
949 "Restoring {} unnamed buffers from recovery",
950 workspace.unnamed_buffers.len()
951 );
952 for unnamed_ref in &workspace.unnamed_buffers {
953 let entries = match self.recovery_service.list_recoverable() {
955 Ok(e) => e,
956 Err(e) => {
957 tracing::warn!("Failed to list recovery entries: {}", e);
958 continue;
959 }
960 };
961
962 let entry = entries.iter().find(|e| e.id == unnamed_ref.recovery_id);
963 if let Some(entry) = entry {
964 match self.recovery_service.load_recovery(entry) {
965 Ok(crate::services::recovery::RecoveryResult::Recovered {
966 content,
967 ..
968 }) => {
969 let text = String::from_utf8_lossy(&content).into_owned();
970 let buffer_id = self.new_buffer();
971 {
972 let state = self.active_state_mut();
973 state.buffer.insert(0, &text);
974 state.buffer.set_modified(true);
976 state.buffer.set_recovery_pending(false);
977 }
978 self.active_event_log_mut().clear_saved_position();
981
982 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
984 meta.recovery_id = Some(unnamed_ref.recovery_id.clone());
985 meta.display_name = unnamed_ref.display_name.clone();
986 }
987
988 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
989 tracing::info!(
990 "Restored unnamed buffer '{}' (recovery_id={})",
991 unnamed_ref.display_name,
992 unnamed_ref.recovery_id
993 );
994 }
995 Ok(other) => {
996 tracing::warn!(
997 "Unexpected recovery result for unnamed buffer {}: {:?}",
998 unnamed_ref.recovery_id,
999 std::mem::discriminant(&other)
1000 );
1001 }
1002 Err(e) => {
1003 tracing::warn!(
1004 "Failed to load recovery for unnamed buffer {}: {}",
1005 unnamed_ref.recovery_id,
1006 e
1007 );
1008 }
1009 }
1010 } else {
1011 tracing::debug!(
1012 "Recovery file not found for unnamed buffer {}",
1013 unnamed_ref.recovery_id
1014 );
1015 }
1016 }
1017 }
1018
1019 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
1021 if !workspace.terminals.is_empty() {
1022 if let Some(ref bridge) = self.async_bridge {
1023 self.terminal_manager.set_async_bridge(bridge.clone());
1024 }
1025 for terminal in &workspace.terminals {
1026 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
1027 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
1028 }
1029 }
1030 }
1031
1032 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
1035 self.restore_split_node(
1036 &workspace.split_layout,
1037 &path_to_buffer,
1038 &terminal_buffer_map,
1039 &unnamed_buffer_map,
1040 &workspace.split_states,
1041 &mut split_id_map,
1042 true, );
1044
1045 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
1049 self.split_manager
1050 .set_active_split(LeafId(new_active_split));
1051 }
1052
1053 for (key, bookmark) in &workspace.bookmarks {
1055 if let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) {
1056 if let Some(buffer) = self.buffers.get(&buffer_id) {
1058 let pos = bookmark.position.min(buffer.buffer.len());
1059 self.bookmarks.set(
1060 *key,
1061 Bookmark {
1062 buffer_id,
1063 position: pos,
1064 },
1065 );
1066 }
1067 }
1068 }
1069
1070 let referenced: HashSet<BufferId> = self
1073 .split_view_states
1074 .values()
1075 .flat_map(|vs| vs.buffer_tab_ids())
1076 .collect();
1077 let orphans: Vec<BufferId> =
1078 self.buffers
1079 .keys()
1080 .copied()
1081 .filter(|id| {
1082 !referenced.contains(id)
1083 && self.buffers.get(id).is_some_and(|s| {
1084 s.buffer.file_path().is_none() && !s.buffer.is_modified()
1085 })
1086 })
1087 .collect();
1088 for id in orphans {
1089 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
1090 self.buffers.remove(&id);
1091 self.event_logs.remove(&id);
1092 self.buffer_metadata.remove(&id);
1093 }
1094
1095 let restored_count = self
1097 .buffers
1098 .keys()
1099 .filter(|id| {
1100 self.buffer_metadata
1101 .get(id)
1102 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
1103 })
1104 .count();
1105 if restored_count > 0 {
1106 let session_label = self
1107 .session_name
1108 .as_ref()
1109 .map(|n| format!("session '{}'", n));
1110 let msg = if let Some(label) = session_label {
1111 format!("Restored {} ({} buffer(s))", label, restored_count)
1112 } else {
1113 format!(
1114 "Restored {} buffer(s) from previous session",
1115 restored_count
1116 )
1117 };
1118 self.set_status_message(msg);
1119 }
1120
1121 tracing::debug!(
1122 "Workspace restore complete: {} splits, {} buffers",
1123 self.split_view_states.len(),
1124 self.buffers.len()
1125 );
1126
1127 #[cfg(feature = "plugins")]
1132 {
1133 let buffer_id = self.active_buffer();
1134 self.update_plugin_state_snapshot();
1135 tracing::debug!(
1136 "Firing buffer_activated for active buffer {:?} after workspace restore",
1137 buffer_id
1138 );
1139 self.plugin_manager.run_hook(
1140 "buffer_activated",
1141 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
1142 );
1143 }
1144
1145 Ok(())
1146 }
1147
1148 fn restore_terminal_from_workspace(
1157 &mut self,
1158 terminal: &SerializedTerminalWorkspace,
1159 ) -> Option<BufferId> {
1160 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
1162 let log_path = if terminal.log_path.is_absolute() {
1163 terminal.log_path.clone()
1164 } else {
1165 terminals_root.join(&terminal.log_path)
1166 };
1167 let backing_path = if terminal.backing_path.is_absolute() {
1168 terminal.backing_path.clone()
1169 } else {
1170 terminals_root.join(&terminal.backing_path)
1171 };
1172
1173 #[allow(clippy::let_underscore_must_use)]
1175 let _ = self.filesystem.create_dir_all(
1176 log_path
1177 .parent()
1178 .or_else(|| backing_path.parent())
1179 .unwrap_or(&terminals_root),
1180 );
1181
1182 let predicted_id = self.terminal_manager.next_terminal_id();
1184 self.terminal_log_files
1185 .insert(predicted_id, log_path.clone());
1186 self.terminal_backing_files
1187 .insert(predicted_id, backing_path.clone());
1188
1189 let terminal_id = match self.terminal_manager.spawn(
1191 terminal.cols,
1192 terminal.rows,
1193 terminal.cwd.clone(),
1194 Some(log_path.clone()),
1195 Some(backing_path.clone()),
1196 ) {
1197 Ok(id) => id,
1198 Err(e) => {
1199 tracing::warn!(
1200 "Failed to restore terminal {}: {}",
1201 terminal.terminal_index,
1202 e
1203 );
1204 return None;
1205 }
1206 };
1207
1208 if terminal_id != predicted_id {
1210 self.terminal_log_files
1211 .insert(terminal_id, log_path.clone());
1212 self.terminal_backing_files
1213 .insert(terminal_id, backing_path.clone());
1214 self.terminal_log_files.remove(&predicted_id);
1215 self.terminal_backing_files.remove(&predicted_id);
1216 }
1217
1218 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1220
1221 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
1224
1225 Some(buffer_id)
1226 }
1227
1228 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
1233 if !backing_path.exists() {
1235 return;
1236 }
1237
1238 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1239 if let Ok(new_state) = EditorState::from_file_with_languages(
1240 backing_path,
1241 self.terminal_width,
1242 self.terminal_height,
1243 large_file_threshold,
1244 &self.grammar_registry,
1245 &self.config.languages,
1246 std::sync::Arc::clone(&self.filesystem),
1247 ) {
1248 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1249 *state = new_state;
1250 let total = state.buffer.total_bytes();
1252 for vs in self.split_view_states.values_mut() {
1254 if vs.has_buffer(buffer_id) {
1255 vs.cursors.primary_mut().position = total;
1256 }
1257 }
1258 state.buffer.set_modified(false);
1260 state.editing_disabled = true;
1262 state.margins.configure_for_line_numbers(false);
1263 }
1264 }
1265 }
1266
1267 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
1269 for (buffer_id, metadata) in &self.buffer_metadata {
1271 if let Some(file_path) = metadata.file_path() {
1272 if file_path == path {
1273 return Ok(*buffer_id);
1274 }
1275 }
1276 }
1277
1278 self.open_file(path).map_err(WorkspaceError::Io)
1280 }
1281
1282 #[allow(clippy::too_many_arguments)]
1284 fn restore_split_node(
1285 &mut self,
1286 node: &SerializedSplitNode,
1287 path_to_buffer: &HashMap<PathBuf, BufferId>,
1288 terminal_buffers: &HashMap<usize, BufferId>,
1289 unnamed_buffers: &HashMap<String, BufferId>,
1290 split_states: &HashMap<usize, SerializedSplitViewState>,
1291 split_id_map: &mut HashMap<usize, SplitId>,
1292 is_first_leaf: bool,
1293 ) {
1294 match node {
1295 SerializedSplitNode::Leaf {
1296 file_path,
1297 split_id,
1298 label,
1299 unnamed_recovery_id,
1300 } => {
1301 let buffer_id = file_path
1303 .as_ref()
1304 .and_then(|p| path_to_buffer.get(p).copied())
1305 .or_else(|| {
1306 unnamed_recovery_id
1307 .as_ref()
1308 .and_then(|id| unnamed_buffers.get(id).copied())
1309 })
1310 .unwrap_or(self.active_buffer());
1311
1312 let current_leaf_id = if is_first_leaf {
1313 let leaf_id = self.split_manager.active_split();
1315 self.split_manager.set_split_buffer(leaf_id, buffer_id);
1316 leaf_id
1317 } else {
1318 self.split_manager.active_split()
1320 };
1321
1322 split_id_map.insert(*split_id, current_leaf_id.into());
1324
1325 if let Some(label) = label {
1327 self.split_manager.set_label(current_leaf_id, label.clone());
1328 }
1329
1330 self.restore_split_view_state(
1332 current_leaf_id,
1333 *split_id,
1334 split_states,
1335 path_to_buffer,
1336 terminal_buffers,
1337 unnamed_buffers,
1338 );
1339 }
1340 SerializedSplitNode::Terminal {
1341 terminal_index,
1342 split_id,
1343 label,
1344 } => {
1345 let buffer_id = terminal_buffers
1346 .get(terminal_index)
1347 .copied()
1348 .unwrap_or(self.active_buffer());
1349
1350 let current_leaf_id = if is_first_leaf {
1351 let leaf_id = self.split_manager.active_split();
1352 self.split_manager.set_split_buffer(leaf_id, buffer_id);
1353 leaf_id
1354 } else {
1355 self.split_manager.active_split()
1356 };
1357
1358 split_id_map.insert(*split_id, current_leaf_id.into());
1359
1360 if let Some(label) = label {
1362 self.split_manager.set_label(current_leaf_id, label.clone());
1363 }
1364
1365 self.split_manager
1366 .set_split_buffer(current_leaf_id, buffer_id);
1367
1368 self.restore_split_view_state(
1369 current_leaf_id,
1370 *split_id,
1371 split_states,
1372 path_to_buffer,
1373 terminal_buffers,
1374 unnamed_buffers,
1375 );
1376 }
1377 SerializedSplitNode::Split {
1378 direction,
1379 first,
1380 second,
1381 ratio,
1382 split_id,
1383 } => {
1384 self.restore_split_node(
1386 first,
1387 path_to_buffer,
1388 terminal_buffers,
1389 unnamed_buffers,
1390 split_states,
1391 split_id_map,
1392 is_first_leaf,
1393 );
1394
1395 let second_buffer_id = get_first_leaf_buffer(
1397 second,
1398 path_to_buffer,
1399 terminal_buffers,
1400 unnamed_buffers,
1401 )
1402 .unwrap_or(self.active_buffer());
1403
1404 let split_direction = match direction {
1406 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1407 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1408 };
1409
1410 match self
1412 .split_manager
1413 .split_active(split_direction, second_buffer_id, *ratio)
1414 {
1415 Ok(new_leaf_id) => {
1416 let mut view_state = SplitViewState::with_buffer(
1418 self.terminal_width,
1419 self.terminal_height,
1420 second_buffer_id,
1421 );
1422 view_state.apply_config_defaults(
1423 self.config.editor.line_numbers,
1424 self.config.editor.highlight_current_line,
1425 self.resolve_line_wrap_for_buffer(second_buffer_id),
1426 self.config.editor.wrap_indent,
1427 self.resolve_wrap_column_for_buffer(second_buffer_id),
1428 self.config.editor.rulers.clone(),
1429 );
1430 self.split_view_states.insert(new_leaf_id, view_state);
1431
1432 split_id_map.insert(*split_id, new_leaf_id.into());
1434
1435 self.restore_split_node(
1437 second,
1438 path_to_buffer,
1439 terminal_buffers,
1440 unnamed_buffers,
1441 split_states,
1442 split_id_map,
1443 false,
1444 );
1445 }
1446 Err(e) => {
1447 tracing::error!("Failed to create split during workspace restore: {}", e);
1448 }
1449 }
1450 }
1451 }
1452 }
1453
1454 fn restore_split_view_state(
1456 &mut self,
1457 current_split_id: LeafId,
1458 saved_split_id: usize,
1459 split_states: &HashMap<usize, SerializedSplitViewState>,
1460 path_to_buffer: &HashMap<PathBuf, BufferId>,
1461 terminal_buffers: &HashMap<usize, BufferId>,
1462 unnamed_buffers: &HashMap<String, BufferId>,
1463 ) {
1464 let Some(split_state) = split_states.get(&saved_split_id) else {
1466 return;
1467 };
1468
1469 let Some(view_state) = self.split_view_states.get_mut(¤t_split_id) else {
1470 return;
1471 };
1472
1473 let mut active_buffer_id: Option<BufferId> = None;
1474
1475 if !split_state.open_tabs.is_empty() {
1476 view_state.open_buffers.clear();
1479
1480 for tab in &split_state.open_tabs {
1481 match tab {
1482 SerializedTabRef::File(rel_path) => {
1483 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1484 if !view_state.has_buffer(buffer_id) {
1485 view_state.add_buffer(buffer_id);
1486 }
1487 view_state.ensure_buffer_state(buffer_id);
1489 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1490 view_state
1491 .buffer_state_mut(buffer_id)
1492 .unwrap()
1493 .viewport
1494 .line_wrap_enabled = false;
1495 }
1496 }
1497 }
1498 SerializedTabRef::Terminal(index) => {
1499 if let Some(&buffer_id) = terminal_buffers.get(index) {
1500 if !view_state.has_buffer(buffer_id) {
1501 view_state.add_buffer(buffer_id);
1502 }
1503 view_state
1504 .ensure_buffer_state(buffer_id)
1505 .viewport
1506 .line_wrap_enabled = false;
1507 }
1508 }
1509 SerializedTabRef::Unnamed(recovery_id) => {
1510 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1511 if !view_state.has_buffer(buffer_id) {
1512 view_state.add_buffer(buffer_id);
1513 }
1514 view_state.ensure_buffer_state(buffer_id);
1515 }
1516 }
1517 }
1518 }
1519
1520 if view_state.open_buffers.is_empty() {
1525 if let Some(buf) = self.split_manager.buffer_for_split(current_split_id) {
1526 view_state.add_buffer(buf);
1527 view_state.ensure_buffer_state(buf);
1528 }
1529 }
1530
1531 if let Some(active_idx) = split_state.active_tab_index {
1532 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1533 active_buffer_id = match tab {
1534 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1535 SerializedTabRef::Terminal(index) => terminal_buffers.get(index).copied(),
1536 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1537 };
1538 }
1539 }
1540 } else {
1541 for rel_path in &split_state.open_files {
1543 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1544 if !view_state.has_buffer(buffer_id) {
1545 view_state.add_buffer(buffer_id);
1546 }
1547 view_state.ensure_buffer_state(buffer_id);
1548 }
1549 }
1550
1551 let active_file_path = split_state.open_files.get(split_state.active_file_index);
1552 active_buffer_id =
1553 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1554 }
1555
1556 for (rel_path, file_state) in &split_state.file_states {
1558 let rel_str = rel_path.to_string_lossy();
1560 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1561 match unnamed_buffers.get(recovery_id).copied() {
1562 Some(id) => id,
1563 None => continue,
1564 }
1565 } else {
1566 match path_to_buffer.get(rel_path).copied() {
1567 Some(id) => id,
1568 None => continue,
1569 }
1570 };
1571 let max_pos = self
1572 .buffers
1573 .get(&buffer_id)
1574 .map(|b| b.buffer.len())
1575 .unwrap_or(0);
1576
1577 let buf_state = view_state.ensure_buffer_state(buffer_id);
1579
1580 let cursor_pos = file_state.cursor.position.min(max_pos);
1581 buf_state.cursors.primary_mut().position = cursor_pos;
1582 buf_state.cursors.primary_mut().anchor =
1583 file_state.cursor.anchor.map(|a| a.min(max_pos));
1584 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1585
1586 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1587 buf_state.viewport.top_view_line_offset = file_state.scroll.top_view_line_offset;
1588 buf_state.viewport.left_column = file_state.scroll.left_column;
1589 buf_state.viewport.set_skip_resize_sync();
1590
1591 buf_state.view_mode = match file_state.view_mode {
1593 SerializedViewMode::Source => ViewMode::Source,
1594 SerializedViewMode::PageView => ViewMode::PageView,
1595 };
1596 buf_state.compose_width = file_state.compose_width;
1597 buf_state.plugin_state = file_state.plugin_state.clone();
1598 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1599 buf_state.folds.clear(&mut state.marker_list);
1600 for fold in &file_state.folds {
1601 let Some(resolved_header) = resolve_fold_header_line(
1608 &state.buffer,
1609 fold.header_line,
1610 fold.header_text.as_deref(),
1611 ) else {
1612 tracing::debug!(
1613 "Dropping stale fold: header_line={} no longer matches stored \
1614 header_text after external edit",
1615 fold.header_line,
1616 );
1617 continue;
1618 };
1619
1620 let shift = resolved_header as i64 - fold.header_line as i64;
1622 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1623 let start_line = resolved_header.saturating_add(1);
1624 let end_line = adjusted_end;
1625 if start_line > end_line {
1626 continue;
1627 }
1628 let Some(start_byte) = state.buffer.line_start_offset(start_line) else {
1629 continue;
1630 };
1631 let end_byte = state
1632 .buffer
1633 .line_start_offset(end_line.saturating_add(1))
1634 .unwrap_or_else(|| state.buffer.len());
1635 buf_state.folds.add(
1636 &mut state.marker_list,
1637 start_byte,
1638 end_byte,
1639 fold.placeholder.clone(),
1640 );
1641 }
1642 }
1643
1644 tracing::trace!(
1645 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1646 rel_path,
1647 cursor_pos,
1648 buf_state.viewport.top_byte,
1649 buf_state.view_mode,
1650 );
1651 }
1652
1653 let restored_view_mode = match split_state.view_mode {
1656 SerializedViewMode::Source => ViewMode::Source,
1657 SerializedViewMode::PageView => ViewMode::PageView,
1658 };
1659
1660 if let Some(active_id) = active_buffer_id {
1661 view_state.switch_buffer(active_id);
1663
1664 let active_has_file_state = split_state
1666 .file_states
1667 .keys()
1668 .any(|rel_path| path_to_buffer.get(rel_path).copied() == Some(active_id));
1669 if !active_has_file_state {
1670 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1671 view_state.active_state_mut().compose_width = split_state.compose_width;
1672 }
1673
1674 self.split_manager
1678 .set_split_buffer(current_split_id, active_id);
1679 }
1680 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1681 }
1682}
1683
1684fn get_first_leaf_buffer(
1686 node: &SerializedSplitNode,
1687 path_to_buffer: &HashMap<PathBuf, BufferId>,
1688 terminal_buffers: &HashMap<usize, BufferId>,
1689 unnamed_buffers: &HashMap<String, BufferId>,
1690) -> Option<BufferId> {
1691 match node {
1692 SerializedSplitNode::Leaf {
1693 file_path,
1694 unnamed_recovery_id,
1695 ..
1696 } => file_path
1697 .as_ref()
1698 .and_then(|p| path_to_buffer.get(p).copied())
1699 .or_else(|| {
1700 unnamed_recovery_id
1701 .as_ref()
1702 .and_then(|id| unnamed_buffers.get(id).copied())
1703 }),
1704 SerializedSplitNode::Terminal { terminal_index, .. } => {
1705 terminal_buffers.get(terminal_index).copied()
1706 }
1707 SerializedSplitNode::Split { first, .. } => {
1708 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
1709 }
1710 }
1711}
1712
1713fn serialize_split_node(
1718 node: &SplitNode,
1719 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1720 working_dir: &Path,
1721 terminal_buffers: &HashMap<BufferId, TerminalId>,
1722 terminal_indices: &HashMap<TerminalId, usize>,
1723 split_labels: &HashMap<SplitId, String>,
1724) -> SerializedSplitNode {
1725 serialize_split_node_pruned(
1726 node,
1727 buffer_metadata,
1728 working_dir,
1729 terminal_buffers,
1730 terminal_indices,
1731 split_labels,
1732 )
1733 .unwrap_or_else(|| {
1734 SerializedSplitNode::Leaf {
1737 file_path: None,
1738 split_id: 0,
1739 label: None,
1740 unnamed_recovery_id: None,
1741 }
1742 })
1743}
1744
1745fn serialize_split_node_pruned(
1752 node: &SplitNode,
1753 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1754 working_dir: &Path,
1755 terminal_buffers: &HashMap<BufferId, TerminalId>,
1756 terminal_indices: &HashMap<TerminalId, usize>,
1757 split_labels: &HashMap<SplitId, String>,
1758) -> Option<SerializedSplitNode> {
1759 match node {
1760 SplitNode::Grouped { layout, .. } => {
1761 serialize_split_node_pruned(
1765 layout,
1766 buffer_metadata,
1767 working_dir,
1768 terminal_buffers,
1769 terminal_indices,
1770 split_labels,
1771 )
1772 }
1773 SplitNode::Leaf {
1774 buffer_id,
1775 split_id,
1776 } => {
1777 let raw_split_id: SplitId = (*split_id).into();
1778 let label = split_labels.get(&raw_split_id).cloned();
1779
1780 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1781 if let Some(index) = terminal_indices.get(terminal_id) {
1782 return Some(SerializedSplitNode::Terminal {
1783 terminal_index: *index,
1784 split_id: raw_split_id.0,
1785 label,
1786 });
1787 }
1788 }
1789
1790 let meta = buffer_metadata.get(buffer_id);
1791
1792 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
1796 return None;
1797 }
1798
1799 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
1800 if abs_path.as_os_str().is_empty() {
1801 None } else {
1803 abs_path
1804 .strip_prefix(working_dir)
1805 .ok()
1806 .map(|p| p.to_path_buf())
1807 }
1808 });
1809
1810 let unnamed_recovery_id = if file_path.is_none() {
1813 meta.and_then(|m| m.recovery_id.clone())
1814 } else {
1815 None
1816 };
1817
1818 Some(SerializedSplitNode::Leaf {
1819 file_path,
1820 split_id: raw_split_id.0,
1821 label,
1822 unnamed_recovery_id,
1823 })
1824 }
1825 SplitNode::Split {
1826 direction,
1827 first,
1828 second,
1829 ratio,
1830 split_id,
1831 ..
1832 } => {
1833 let raw_split_id: SplitId = (*split_id).into();
1834 let first = serialize_split_node_pruned(
1835 first,
1836 buffer_metadata,
1837 working_dir,
1838 terminal_buffers,
1839 terminal_indices,
1840 split_labels,
1841 );
1842 let second = serialize_split_node_pruned(
1843 second,
1844 buffer_metadata,
1845 working_dir,
1846 terminal_buffers,
1847 terminal_indices,
1848 split_labels,
1849 );
1850 match (first, second) {
1851 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
1852 direction: match direction {
1853 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
1854 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
1855 },
1856 first: Box::new(f),
1857 second: Box::new(s),
1858 ratio: *ratio,
1859 split_id: raw_split_id.0,
1860 }),
1861 (Some(only), None) | (None, Some(only)) => Some(only),
1864 (None, None) => None,
1865 }
1866 }
1867 }
1868}
1869
1870fn serialize_split_view_state(
1871 view_state: &crate::view::split::SplitViewState,
1872 buffers: &HashMap<BufferId, EditorState>,
1873 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1874 working_dir: &Path,
1875 active_buffer: Option<BufferId>,
1876 terminal_buffers: &HashMap<BufferId, TerminalId>,
1877 terminal_indices: &HashMap<TerminalId, usize>,
1878) -> SerializedSplitViewState {
1879 let mut open_tabs = Vec::new();
1880 let mut open_files = Vec::new();
1881 let mut active_tab_index = None;
1882
1883 for buffer_id in view_state.buffer_tab_ids() {
1885 let buffer_id = &buffer_id;
1886 let tab_index = open_tabs.len();
1887 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1888 if let Some(idx) = terminal_indices.get(terminal_id) {
1889 open_tabs.push(SerializedTabRef::Terminal(*idx));
1890 if Some(*buffer_id) == active_buffer {
1891 active_tab_index = Some(tab_index);
1892 }
1893 continue;
1894 }
1895 }
1896
1897 if let Some(meta) = buffer_metadata.get(buffer_id) {
1898 if let Some(abs_path) = meta.file_path() {
1899 if abs_path.as_os_str().is_empty() {
1900 if let Some(ref recovery_id) = meta.recovery_id {
1902 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
1903 if Some(*buffer_id) == active_buffer {
1904 active_tab_index = Some(tab_index);
1905 }
1906 }
1907 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
1908 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
1909 open_files.push(rel_path.to_path_buf());
1910 if Some(*buffer_id) == active_buffer {
1911 active_tab_index = Some(tab_index);
1912 }
1913 } else {
1914 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
1916 if Some(*buffer_id) == active_buffer {
1917 active_tab_index = Some(tab_index);
1918 }
1919 }
1920 }
1921 }
1922 }
1923
1924 let active_file_index = active_tab_index
1926 .and_then(|idx| open_tabs.get(idx))
1927 .and_then(|tab| match tab {
1928 SerializedTabRef::File(path) => {
1929 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
1930 }
1931 _ => None,
1932 })
1933 .unwrap_or(0);
1934
1935 let mut file_states = HashMap::new();
1937 for (buffer_id, buf_state) in &view_state.keyed_states {
1938 let Some(meta) = buffer_metadata.get(buffer_id) else {
1939 continue;
1940 };
1941 let Some(abs_path) = meta.file_path() else {
1942 continue;
1943 };
1944
1945 let state_key = if abs_path.as_os_str().is_empty() {
1947 if let Some(ref recovery_id) = meta.recovery_id {
1949 PathBuf::from(format!("__unnamed__{}", recovery_id))
1950 } else {
1951 continue;
1952 }
1953 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
1954 rp.to_path_buf()
1955 } else {
1956 abs_path.to_path_buf()
1958 };
1959
1960 let primary_cursor = buf_state.cursors.primary();
1961 let folds = buffers
1962 .get(buffer_id)
1963 .map(|state| {
1964 buf_state
1965 .folds
1966 .collapsed_line_ranges(&state.buffer, &state.marker_list)
1967 .into_iter()
1968 .map(|range| SerializedFoldRange {
1969 header_line: range.header_line,
1970 end_line: range.end_line,
1971 placeholder: range.placeholder,
1972 header_text: range.header_text,
1973 })
1974 .collect::<Vec<_>>()
1975 })
1976 .unwrap_or_default();
1977
1978 file_states.insert(
1979 state_key,
1980 SerializedFileState {
1981 cursor: SerializedCursor {
1982 position: primary_cursor.position,
1983 anchor: primary_cursor.anchor,
1984 sticky_column: primary_cursor.sticky_column,
1985 },
1986 additional_cursors: buf_state
1987 .cursors
1988 .iter()
1989 .skip(1) .map(|(_, cursor)| SerializedCursor {
1991 position: cursor.position,
1992 anchor: cursor.anchor,
1993 sticky_column: cursor.sticky_column,
1994 })
1995 .collect(),
1996 scroll: SerializedScroll {
1997 top_byte: buf_state.viewport.top_byte,
1998 top_view_line_offset: buf_state.viewport.top_view_line_offset,
1999 left_column: buf_state.viewport.left_column,
2000 },
2001 view_mode: match buf_state.view_mode {
2002 ViewMode::Source => SerializedViewMode::Source,
2003 ViewMode::PageView => SerializedViewMode::PageView,
2004 },
2005 compose_width: buf_state.compose_width,
2006 plugin_state: buf_state.plugin_state.clone(),
2007 folds,
2008 },
2009 );
2010 }
2011
2012 let active_view_mode = active_buffer
2014 .and_then(|id| view_state.keyed_states.get(&id))
2015 .map(|bs| match bs.view_mode {
2016 ViewMode::Source => SerializedViewMode::Source,
2017 ViewMode::PageView => SerializedViewMode::PageView,
2018 })
2019 .unwrap_or(SerializedViewMode::Source);
2020 let active_compose_width = active_buffer
2021 .and_then(|id| view_state.keyed_states.get(&id))
2022 .and_then(|bs| bs.compose_width);
2023
2024 SerializedSplitViewState {
2025 open_tabs,
2026 active_tab_index,
2027 open_files,
2028 active_file_index,
2029 file_states,
2030 tab_scroll_offset: view_state.tab_scroll_offset,
2031 view_mode: active_view_mode,
2032 compose_width: active_compose_width,
2033 }
2034}
2035
2036fn serialize_bookmarks(
2037 bookmarks: &BookmarkState,
2038 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2039 working_dir: &Path,
2040) -> HashMap<char, SerializedBookmark> {
2041 bookmarks
2042 .iter()
2043 .filter_map(|(key, bookmark)| {
2044 buffer_metadata
2045 .get(&bookmark.buffer_id)
2046 .and_then(|meta| meta.file_path())
2047 .and_then(|abs_path| {
2048 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2049 (
2050 key,
2051 SerializedBookmark {
2052 file_path: rel_path.to_path_buf(),
2053 position: bookmark.position,
2054 },
2055 )
2056 })
2057 })
2058 })
2059 .collect()
2060}
2061
2062fn collect_file_paths_from_states(
2064 split_states: &HashMap<usize, SerializedSplitViewState>,
2065) -> Vec<PathBuf> {
2066 let mut paths = Vec::new();
2067 for state in split_states.values() {
2068 if !state.open_tabs.is_empty() {
2069 for tab in &state.open_tabs {
2070 if let SerializedTabRef::File(path) = tab {
2071 if !paths.contains(path) {
2072 paths.push(path.clone());
2073 }
2074 }
2075 }
2076 } else {
2077 for path in &state.open_files {
2078 if !paths.contains(path) {
2079 paths.push(path.clone());
2080 }
2081 }
2082 }
2083 }
2084 paths
2085}
2086
2087fn get_expanded_dirs(
2089 explorer: &crate::view::file_tree::FileTreeView,
2090 working_dir: &Path,
2091) -> Vec<PathBuf> {
2092 let mut expanded = Vec::new();
2093 let tree = explorer.tree();
2094
2095 for node in tree.all_nodes() {
2097 if node.is_expanded() && node.is_dir() {
2098 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2100 expanded.push(rel_path.to_path_buf());
2101 }
2102 }
2103 }
2104
2105 expanded
2106}