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