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 side: self.file_explorer_side,
263 expanded_dirs,
264 scroll_offset: explorer.get_scroll_offset(),
265 show_hidden: explorer.ignore_patterns().show_hidden(),
266 show_gitignored: explorer.ignore_patterns().show_gitignored(),
267 }
268 } else {
269 FileExplorerState {
270 visible: self.file_explorer_visible,
271 width: self.file_explorer_width,
272 side: self.file_explorer_side,
273 expanded_dirs: Vec::new(),
274 scroll_offset: 0,
275 show_hidden: false,
276 show_gitignored: false,
277 }
278 };
279
280 let config_overrides = WorkspaceConfigOverrides {
285 line_numbers: Some(self.config.editor.line_numbers),
286 relative_line_numbers: Some(self.config.editor.relative_line_numbers),
287 line_wrap: Some(self.config.editor.line_wrap),
288 syntax_highlighting: Some(self.config.editor.syntax_highlighting),
289 enable_inlay_hints: Some(self.config.editor.enable_inlay_hints),
290 mouse_enabled: Some(self.mouse_enabled),
291 menu_bar_hidden: None,
292 };
293
294 let histories = WorkspaceHistories {
296 search: self
297 .prompt_histories
298 .get("search")
299 .map(|h| h.items().to_vec())
300 .unwrap_or_default(),
301 replace: self
302 .prompt_histories
303 .get("replace")
304 .map(|h| h.items().to_vec())
305 .unwrap_or_default(),
306 command_palette: Vec::new(), goto_line: self
308 .prompt_histories
309 .get("goto_line")
310 .map(|h| h.items().to_vec())
311 .unwrap_or_default(),
312 open_file: Vec::new(), };
314 tracing::trace!(
315 "Captured histories: {} search, {} replace",
316 histories.search.len(),
317 histories.replace.len()
318 );
319
320 let search_options = SearchOptions {
322 case_sensitive: self.search_case_sensitive,
323 whole_word: self.search_whole_word,
324 use_regex: self.search_use_regex,
325 confirm_each: self.search_confirm_each,
326 };
327
328 let bookmarks =
330 serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.working_dir);
331
332 let external_files: Vec<PathBuf> = self
335 .buffer_metadata
336 .values()
337 .filter_map(|meta| meta.file_path())
338 .filter(|abs_path| abs_path.strip_prefix(&self.working_dir).is_err())
339 .cloned()
340 .collect();
341 if !external_files.is_empty() {
342 tracing::debug!("Captured {} external files", external_files.len());
343 }
344
345 let read_only_files: Vec<PathBuf> = self
349 .buffer_metadata
350 .values()
351 .filter(|meta| meta.read_only)
352 .filter_map(|meta| meta.file_path().cloned())
353 .filter(|p| !p.as_os_str().is_empty())
354 .map(|p| {
355 p.strip_prefix(&self.working_dir)
356 .map(|rel| rel.to_path_buf())
357 .unwrap_or(p)
358 })
359 .collect();
360
361 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.config.editor.hot_exit {
363 self.buffer_metadata
364 .iter()
365 .filter_map(|(buffer_id, meta)| {
366 let path = meta.file_path()?;
368 if !path.as_os_str().is_empty() {
369 return None;
370 }
371 if meta.hidden_from_tabs || meta.is_virtual() {
373 return None;
374 }
375 let state = self.buffers.get(buffer_id)?;
377 if state.buffer.total_bytes() == 0 {
378 return None;
379 }
380 let recovery_id = meta.recovery_id.clone()?;
382 Some(UnnamedBufferRef {
383 recovery_id,
384 display_name: meta.display_name.clone(),
385 })
386 })
387 .collect()
388 } else {
389 Vec::new()
390 };
391 if !unnamed_buffers.is_empty() {
392 tracing::debug!("Captured {} unnamed buffers", unnamed_buffers.len());
393 }
394
395 Workspace {
396 version: WORKSPACE_VERSION,
397 working_dir: self.working_dir.clone(),
398 split_layout,
399 active_split_id: SplitId::from(self.split_manager.active_split()).0,
400 split_states,
401 config_overrides,
402 file_explorer,
403 histories,
404 search_options,
405 bookmarks,
406 terminals,
407 external_files,
408 read_only_files,
409 unnamed_buffers,
410 plugin_global_state: self.plugin_global_state.clone(),
411 saved_at: std::time::SystemTime::now()
412 .duration_since(std::time::UNIX_EPOCH)
413 .unwrap_or_default()
414 .as_secs(),
415 }
416 }
417
418 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
424 self.sync_all_terminal_backing_files();
426
427 self.save_all_global_file_states();
429
430 let workspace = self.capture_workspace();
431
432 if let Some(ref session_name) = self.session_name {
434 workspace.save_session(session_name)
435 } else {
436 workspace.save()
437 }
438 }
439
440 fn save_all_global_file_states(&self) {
442 for (leaf_id, view_state) in &self.split_view_states {
444 let active_buffer = self
446 .split_manager
447 .root()
448 .get_leaves_with_rects(ratatui::layout::Rect::default())
449 .into_iter()
450 .find(|(sid, _, _)| *sid == *leaf_id)
451 .map(|(_, buffer_id, _)| buffer_id);
452
453 if let Some(buffer_id) = active_buffer {
454 self.save_buffer_file_state(buffer_id, view_state);
455 }
456 }
457 }
458
459 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
461 let abs_path = match self.buffer_metadata.get(&buffer_id) {
463 Some(metadata) => match metadata.file_path() {
464 Some(path) => path.to_path_buf(),
465 None => return, },
467 None => return,
468 };
469
470 let primary_cursor = view_state.cursors.primary();
472 let file_state = SerializedFileState {
473 cursor: SerializedCursor {
474 position: primary_cursor.position,
475 anchor: primary_cursor.anchor,
476 sticky_column: primary_cursor.sticky_column,
477 },
478 additional_cursors: view_state
479 .cursors
480 .iter()
481 .skip(1)
482 .map(|(_, cursor)| SerializedCursor {
483 position: cursor.position,
484 anchor: cursor.anchor,
485 sticky_column: cursor.sticky_column,
486 })
487 .collect(),
488 scroll: SerializedScroll {
489 top_byte: view_state.viewport.top_byte,
490 top_view_line_offset: view_state.viewport.top_view_line_offset,
491 left_column: view_state.viewport.left_column,
492 },
493 view_mode: Default::default(),
494 compose_width: None,
495 plugin_state: std::collections::HashMap::new(),
496 folds: Vec::new(),
497 };
498
499 PersistedFileWorkspace::save(&abs_path, file_state);
501 }
502
503 fn sync_all_terminal_backing_files(&mut self) {
508 use std::io::BufWriter;
509
510 let terminals_to_sync: Vec<_> = self
512 .terminal_buffers
513 .values()
514 .copied()
515 .filter_map(|terminal_id| {
516 self.terminal_backing_files
517 .get(&terminal_id)
518 .map(|path| (terminal_id, path.clone()))
519 })
520 .collect();
521
522 for (terminal_id, backing_path) in terminals_to_sync {
523 if let Some(handle) = self.terminal_manager.get(terminal_id) {
524 if let Ok(state) = handle.state.lock() {
525 if let Ok(mut file) = self
527 .authority
528 .filesystem
529 .open_file_for_append(&backing_path)
530 {
531 let mut writer = BufWriter::new(&mut *file);
532 if let Err(e) = state.append_visible_screen(&mut writer) {
533 tracing::warn!(
534 "Failed to sync terminal {:?} to backing file: {}",
535 terminal_id,
536 e
537 );
538 }
539 }
540 }
541 }
542 }
543 }
544
545 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
549 tracing::debug!("Attempting to restore workspace for {:?}", self.working_dir);
550
551 let workspace = if let Some(ref session_name) = self.session_name {
553 Workspace::load_session(session_name, &self.working_dir)?
554 } else {
555 Workspace::load(&self.working_dir)?
556 };
557
558 match workspace {
559 Some(workspace) => {
560 tracing::info!("Found workspace, applying...");
561 self.apply_workspace(&workspace)?;
562 Ok(true)
563 }
564 None => {
565 tracing::debug!("No workspace found for {:?}", self.working_dir);
566 Ok(false)
567 }
568 }
569 }
570
571 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
577 if !self.config.editor.hot_exit {
578 return Ok(0);
579 }
580
581 let entries = self.recovery_service.list_recoverable()?;
582 if entries.is_empty() {
583 return Ok(0);
584 }
585
586 let buffer_files: Vec<_> = self
588 .buffers
589 .iter()
590 .filter_map(|(buffer_id, state)| {
591 let path = state.buffer.file_path()?.to_path_buf();
592 if path.as_os_str().is_empty() {
593 return None; }
595 Some((*buffer_id, path))
596 })
597 .collect();
598
599 let mut recovered = 0;
600 for (buffer_id, file_path) in buffer_files {
601 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
602 let entry = entries.iter().find(|e| e.id == recovery_id);
603 if let Some(entry) = entry {
604 match self.recovery_service.load_recovery(entry) {
605 Ok(crate::services::recovery::RecoveryResult::Recovered {
606 content, ..
607 }) => {
608 let mut mutated = false;
609 if let Some(state) = self.buffers.get_mut(&buffer_id) {
610 let current_len = state.buffer.total_bytes();
611 let text = String::from_utf8_lossy(&content).into_owned();
612 let current = state.buffer.get_text_range_mut(0, current_len).ok();
613 let current_text = current
614 .as_ref()
615 .map(|b| String::from_utf8_lossy(b).into_owned());
616 if current_text.as_deref() != Some(&text) {
617 state.buffer.delete(0..current_len);
618 state.buffer.insert(0, &text);
619 state.buffer.set_modified(true);
620 state.buffer.set_recovery_pending(false);
621 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
624 log.clear_saved_position();
625 }
626 mutated = true;
627 recovered += 1;
628 tracing::info!(
629 "Restored unsaved changes for {:?} from hot exit recovery",
630 file_path
631 );
632 }
633 }
634 if mutated {
635 self.sync_lsp_after_recovery_replay(buffer_id);
636 }
637 }
638 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
639 chunks,
640 ..
641 }) => {
642 let mut mutated = false;
643 if let Some(state) = self.buffers.get_mut(&buffer_id) {
644 for chunk in chunks.into_iter().rev() {
645 let text = String::from_utf8_lossy(&chunk.content).into_owned();
646 if chunk.original_len > 0 {
647 state
648 .buffer
649 .delete(chunk.offset..chunk.offset + chunk.original_len);
650 }
651 state.buffer.insert(chunk.offset, &text);
652 }
653 state.buffer.set_modified(true);
654 state.buffer.set_recovery_pending(false);
655 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
658 log.clear_saved_position();
659 }
660 mutated = true;
661 recovered += 1;
662 tracing::info!(
663 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
664 file_path
665 );
666 }
667 if mutated {
668 self.sync_lsp_after_recovery_replay(buffer_id);
669 }
670 }
671 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
672 original_path,
673 ..
674 }) => {
675 let name = original_path
676 .file_name()
677 .unwrap_or_default()
678 .to_string_lossy();
679 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
680 self.set_status_message(format!(
681 "{} changed on disk; unsaved changes not restored",
682 name
683 ));
684 }
685 Ok(_) => {} Err(e) => {
687 tracing::debug!(
688 "Failed to load hot exit recovery for {:?}: {}",
689 file_path,
690 e
691 );
692 }
693 }
694 }
695 }
696
697 Ok(recovered)
698 }
699
700 pub fn apply_workspace(&mut self, workspace: &Workspace) -> Result<(), WorkspaceError> {
702 tracing::debug!(
703 "Applying workspace with {} split states",
704 workspace.split_states.len()
705 );
706
707 self.restore_config_overrides(&workspace.config_overrides);
708
709 if !workspace.plugin_global_state.is_empty() {
710 tracing::debug!(
711 "Restoring plugin global state for {} plugins",
712 workspace.plugin_global_state.len()
713 );
714 self.plugin_global_state = workspace.plugin_global_state.clone();
715 }
716
717 self.restore_search_options(&workspace.search_options);
718 self.restore_prompt_histories(&workspace.histories);
719 self.restore_file_explorer_settings(&workspace.file_explorer);
720
721 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
722 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
723 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
724 self.restore_hot_exit_changes(&path_to_buffer);
725
726 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
727 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
728
729 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
730 self.restore_split_node(
731 &workspace.split_layout,
732 &path_to_buffer,
733 &terminal_buffer_map,
734 &unnamed_buffer_map,
735 &workspace.split_states,
736 &mut split_id_map,
737 true,
738 );
739
740 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
741 self.split_manager
742 .set_active_split(LeafId(new_active_split));
743 }
744
745 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
746 self.clean_orphaned_buffers();
747 self.log_restore_summary();
748
749 #[cfg(feature = "plugins")]
750 {
751 let buffer_id = self.active_buffer();
752 self.update_plugin_state_snapshot();
753 tracing::debug!(
754 "Firing buffer_activated for active buffer {:?} after workspace restore",
755 buffer_id
756 );
757 self.plugin_manager.run_hook(
758 "buffer_activated",
759 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
760 );
761 }
762
763 Ok(())
764 }
765
766 fn restore_config_overrides(&mut self, overrides: &WorkspaceConfigOverrides) {
767 if let Some(line_numbers) = overrides.line_numbers {
768 self.config_mut().editor.line_numbers = line_numbers;
769 }
770 if let Some(relative_line_numbers) = overrides.relative_line_numbers {
771 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
772 }
773 if let Some(line_wrap) = overrides.line_wrap {
774 self.config_mut().editor.line_wrap = line_wrap;
775 }
776 if let Some(syntax_highlighting) = overrides.syntax_highlighting {
777 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
778 }
779 if let Some(enable_inlay_hints) = overrides.enable_inlay_hints {
780 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
781 }
782 if let Some(mouse_enabled) = overrides.mouse_enabled {
783 self.mouse_enabled = mouse_enabled;
784 }
785 }
790
791 fn restore_search_options(&mut self, opts: &SearchOptions) {
792 self.search_case_sensitive = opts.case_sensitive;
793 self.search_whole_word = opts.whole_word;
794 self.search_use_regex = opts.use_regex;
795 self.search_confirm_each = opts.confirm_each;
796 }
797
798 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
799 tracing::debug!(
800 "Restoring histories: {} search, {} replace, {} goto_line",
801 histories.search.len(),
802 histories.replace.len(),
803 histories.goto_line.len()
804 );
805 for item in &histories.search {
806 self.get_or_create_prompt_history("search")
807 .push(item.clone());
808 }
809 for item in &histories.replace {
810 self.get_or_create_prompt_history("replace")
811 .push(item.clone());
812 }
813 for item in &histories.goto_line {
814 self.get_or_create_prompt_history("goto_line")
815 .push(item.clone());
816 }
817 }
818
819 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
820 self.file_explorer_visible = fe.visible;
821 self.file_explorer_width = fe.width;
822 self.file_explorer_side = fe.side;
823
824 if fe.show_hidden {
826 self.pending_file_explorer_show_hidden = Some(true);
827 }
828 if fe.show_gitignored {
829 self.pending_file_explorer_show_gitignored = Some(true);
830 }
831
832 if self.file_explorer_visible && self.file_explorer.is_none() {
834 self.init_file_explorer();
835 }
836 }
837
838 fn open_workspace_files(
841 &mut self,
842 split_states: &HashMap<usize, SerializedSplitViewState>,
843 ) -> HashMap<PathBuf, BufferId> {
844 let file_paths = collect_file_paths_from_states(split_states);
845 tracing::debug!(
846 "Workspace has {} files to restore: {:?}",
847 file_paths.len(),
848 file_paths
849 );
850 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
851 for rel_path in file_paths {
852 let abs_path = self.working_dir.join(&rel_path);
853 tracing::trace!(
854 "Checking file: {:?} (exists: {})",
855 abs_path,
856 abs_path.exists()
857 );
858 if abs_path.exists() {
859 match self.open_file_internal(&abs_path) {
860 Ok(buffer_id) => {
861 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
862 path_to_buffer.insert(rel_path, buffer_id);
863 }
864 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
865 }
866 } else {
867 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
868 }
869 }
870 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
871 path_to_buffer
872 }
873
874 fn restore_external_files(
876 &mut self,
877 external_files: &[PathBuf],
878 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
879 ) {
880 if external_files.is_empty() {
881 return;
882 }
883 tracing::debug!(
884 "Restoring {} external files: {:?}",
885 external_files.len(),
886 external_files
887 );
888 for abs_path in external_files {
889 if !abs_path.exists() {
890 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
891 continue;
892 }
893 match self.open_file_internal(abs_path) {
894 Ok(buffer_id) => {
895 path_to_buffer.insert(abs_path.clone(), buffer_id);
896 tracing::debug!(
897 "Restored external file {:?} as buffer {:?}",
898 abs_path,
899 buffer_id
900 );
901 }
902 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
903 }
904 }
905 }
906
907 fn apply_read_only_flags(
910 &mut self,
911 read_only_files: &[PathBuf],
912 path_to_buffer: &HashMap<PathBuf, BufferId>,
913 ) {
914 for ro_path in read_only_files {
915 let buffer_id = path_to_buffer
916 .get(ro_path)
917 .copied()
918 .or_else(|| path_to_buffer.get(&self.working_dir.join(ro_path)).copied());
919 if let Some(id) = buffer_id {
920 self.mark_buffer_read_only(id, true);
921 }
922 }
923 }
924
925 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
928 if !self.config.editor.hot_exit {
929 return;
930 }
931 let entries = self.recovery_service.list_recoverable().unwrap_or_default();
932 if entries.is_empty() {
933 return;
934 }
935 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
936 for buffer_id in buffer_ids {
937 let file_path = self
938 .buffers
939 .get(&buffer_id)
940 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
941 let Some(file_path) = file_path else { continue };
942
943 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
944 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
945 continue;
946 };
947 match self.recovery_service.load_recovery(entry) {
948 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
949 let mut mutated = false;
950 if let Some(state) = self.buffers.get_mut(&buffer_id) {
951 let current_len = state.buffer.total_bytes();
952 let text = String::from_utf8_lossy(&content).into_owned();
953 let current = state.buffer.get_text_range_mut(0, current_len).ok();
954 let current_text = current
955 .as_ref()
956 .map(|b| String::from_utf8_lossy(b).into_owned());
957 if current_text.as_deref() != Some(&text) {
958 state.buffer.delete(0..current_len);
959 state.buffer.insert(0, &text);
960 state.buffer.set_modified(true);
961 state.buffer.set_recovery_pending(false);
962 mutated = true;
963 tracing::info!(
964 "Restored unsaved changes for {:?} from hot exit recovery",
965 file_path
966 );
967 }
968 }
969 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
970 log.clear_saved_position();
971 }
972 if mutated {
973 self.sync_lsp_after_recovery_replay(buffer_id);
974 }
975 }
976 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
977 chunks, ..
978 }) => {
979 let mut mutated = false;
980 if let Some(state) = self.buffers.get_mut(&buffer_id) {
981 for chunk in chunks.into_iter().rev() {
982 let text = String::from_utf8_lossy(&chunk.content).into_owned();
983 if chunk.original_len > 0 {
984 state
985 .buffer
986 .delete(chunk.offset..chunk.offset + chunk.original_len);
987 }
988 state.buffer.insert(chunk.offset, &text);
989 }
990 state.buffer.set_modified(true);
991 state.buffer.set_recovery_pending(false);
992 mutated = true;
993 tracing::info!(
994 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
995 file_path
996 );
997 }
998 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
999 log.clear_saved_position();
1000 }
1001 if mutated {
1002 self.sync_lsp_after_recovery_replay(buffer_id);
1003 }
1004 }
1005 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1006 original_path,
1007 ..
1008 }) => {
1009 let name = original_path
1010 .file_name()
1011 .unwrap_or_default()
1012 .to_string_lossy();
1013 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1014 self.set_status_message(format!(
1015 "{} changed on disk; unsaved changes not restored",
1016 name
1017 ));
1018 }
1019 Ok(_) => {} Err(e) => {
1021 tracing::debug!(
1022 "Failed to load hot exit recovery for {:?}: {}",
1023 file_path,
1024 e
1025 );
1026 }
1027 }
1028 }
1029 }
1030
1031 fn restore_unnamed_buffers(
1034 &mut self,
1035 unnamed_buffers: &[UnnamedBufferRef],
1036 ) -> HashMap<String, BufferId> {
1037 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1038 if !self.config.editor.hot_exit || unnamed_buffers.is_empty() {
1039 return unnamed_buffer_map;
1040 }
1041 tracing::debug!(
1042 "Restoring {} unnamed buffers from recovery",
1043 unnamed_buffers.len()
1044 );
1045 for unnamed_ref in unnamed_buffers {
1046 let entries = match self.recovery_service.list_recoverable() {
1047 Ok(e) => e,
1048 Err(e) => {
1049 tracing::warn!("Failed to list recovery entries: {}", e);
1050 continue;
1051 }
1052 };
1053 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1054 tracing::debug!(
1055 "Recovery file not found for unnamed buffer {}",
1056 unnamed_ref.recovery_id
1057 );
1058 continue;
1059 };
1060 match self.recovery_service.load_recovery(entry) {
1061 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1062 let text = String::from_utf8_lossy(&content).into_owned();
1063 let buffer_id = self.new_buffer();
1064 {
1065 let state = self.active_state_mut();
1066 state.buffer.insert(0, &text);
1067 state.buffer.set_modified(true);
1068 state.buffer.set_recovery_pending(false);
1069 }
1070 self.active_event_log_mut().clear_saved_position();
1071 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
1072 meta.recovery_id = Some(unnamed_ref.recovery_id.clone());
1073 meta.display_name = unnamed_ref.display_name.clone();
1074 }
1075 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1076 tracing::info!(
1077 "Restored unnamed buffer '{}' (recovery_id={})",
1078 unnamed_ref.display_name,
1079 unnamed_ref.recovery_id
1080 );
1081 }
1082 Ok(other) => {
1083 tracing::warn!(
1084 "Unexpected recovery result for unnamed buffer {}: {:?}",
1085 unnamed_ref.recovery_id,
1086 std::mem::discriminant(&other)
1087 );
1088 }
1089 Err(e) => {
1090 tracing::warn!(
1091 "Failed to load recovery for unnamed buffer {}: {}",
1092 unnamed_ref.recovery_id,
1093 e
1094 );
1095 }
1096 }
1097 }
1098 unnamed_buffer_map
1099 }
1100
1101 fn restore_terminals_from_workspace(
1103 &mut self,
1104 terminals: &[SerializedTerminalWorkspace],
1105 ) -> HashMap<usize, BufferId> {
1106 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
1107 if terminals.is_empty() {
1108 return terminal_buffer_map;
1109 }
1110 if let Some(ref bridge) = self.async_bridge {
1111 self.terminal_manager.set_async_bridge(bridge.clone());
1112 }
1113 for terminal in terminals {
1114 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
1115 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
1116 }
1117 }
1118 terminal_buffer_map
1119 }
1120
1121 fn restore_bookmarks_from_workspace(
1123 &mut self,
1124 bookmarks: &HashMap<char, SerializedBookmark>,
1125 path_to_buffer: &HashMap<PathBuf, BufferId>,
1126 ) {
1127 for (key, bookmark) in bookmarks {
1128 let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) else {
1129 continue;
1130 };
1131 if let Some(buffer) = self.buffers.get(&buffer_id) {
1132 let pos = bookmark.position.min(buffer.buffer.len());
1133 self.bookmarks.set(
1134 *key,
1135 Bookmark {
1136 buffer_id,
1137 position: pos,
1138 },
1139 );
1140 }
1141 }
1142 }
1143
1144 fn clean_orphaned_buffers(&mut self) {
1147 let referenced: HashSet<BufferId> = self
1148 .split_view_states
1149 .values()
1150 .flat_map(|vs| vs.buffer_tab_ids())
1151 .collect();
1152 let orphans: Vec<BufferId> =
1153 self.buffers
1154 .keys()
1155 .copied()
1156 .filter(|id| {
1157 !referenced.contains(id)
1158 && self.buffers.get(id).is_some_and(|s| {
1159 s.buffer.file_path().is_none() && !s.buffer.is_modified()
1160 })
1161 })
1162 .collect();
1163 for id in orphans {
1164 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
1165 self.buffers.remove(&id);
1166 self.event_logs.remove(&id);
1167 self.buffer_metadata.remove(&id);
1168 }
1169 }
1170
1171 fn log_restore_summary(&mut self) {
1174 tracing::debug!(
1175 "Workspace restore complete: {} splits, {} buffers",
1176 self.split_view_states.len(),
1177 self.buffers.len()
1178 );
1179 let restored_count = self
1180 .buffers
1181 .keys()
1182 .filter(|id| {
1183 self.buffer_metadata
1184 .get(id)
1185 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
1186 })
1187 .count();
1188 if restored_count == 0 {
1189 return;
1190 }
1191 let msg = match self
1192 .session_name
1193 .as_ref()
1194 .map(|n| format!("session '{}'", n))
1195 {
1196 Some(label) => format!("Restored {} ({} buffer(s))", label, restored_count),
1197 None => format!(
1198 "Restored {} buffer(s) from previous session",
1199 restored_count
1200 ),
1201 };
1202 self.set_status_message(msg);
1203 }
1204
1205 fn restore_terminal_from_workspace(
1214 &mut self,
1215 terminal: &SerializedTerminalWorkspace,
1216 ) -> Option<BufferId> {
1217 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
1219 let log_path = if terminal.log_path.is_absolute() {
1220 terminal.log_path.clone()
1221 } else {
1222 terminals_root.join(&terminal.log_path)
1223 };
1224 let backing_path = if terminal.backing_path.is_absolute() {
1225 terminal.backing_path.clone()
1226 } else {
1227 terminals_root.join(&terminal.backing_path)
1228 };
1229
1230 #[allow(clippy::let_underscore_must_use)]
1232 let _ = self.authority.filesystem.create_dir_all(
1233 log_path
1234 .parent()
1235 .or_else(|| backing_path.parent())
1236 .unwrap_or(&terminals_root),
1237 );
1238
1239 let predicted_id = self.terminal_manager.next_terminal_id();
1241 self.terminal_log_files
1242 .insert(predicted_id, log_path.clone());
1243 self.terminal_backing_files
1244 .insert(predicted_id, backing_path.clone());
1245
1246 let terminal_id = match self.terminal_manager.spawn(
1248 terminal.cols,
1249 terminal.rows,
1250 terminal.cwd.clone(),
1251 Some(log_path.clone()),
1252 Some(backing_path.clone()),
1253 self.resolved_terminal_wrapper(),
1254 ) {
1255 Ok(id) => id,
1256 Err(e) => {
1257 tracing::warn!(
1258 "Failed to restore terminal {}: {}",
1259 terminal.terminal_index,
1260 e
1261 );
1262 return None;
1263 }
1264 };
1265
1266 if terminal_id != predicted_id {
1268 self.terminal_log_files
1269 .insert(terminal_id, log_path.clone());
1270 self.terminal_backing_files
1271 .insert(terminal_id, backing_path.clone());
1272 self.terminal_log_files.remove(&predicted_id);
1273 self.terminal_backing_files.remove(&predicted_id);
1274 }
1275
1276 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1278
1279 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
1282
1283 Some(buffer_id)
1284 }
1285
1286 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
1291 if !backing_path.exists() {
1293 return;
1294 }
1295
1296 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1297 if let Ok(new_state) = EditorState::from_file_with_languages(
1298 backing_path,
1299 self.terminal_width,
1300 self.terminal_height,
1301 large_file_threshold,
1302 &self.grammar_registry,
1303 &self.config.languages,
1304 std::sync::Arc::clone(&self.authority.filesystem),
1305 ) {
1306 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1307 *state = new_state;
1308 let total = state.buffer.total_bytes();
1310 for vs in self.split_view_states.values_mut() {
1312 if vs.has_buffer(buffer_id) {
1313 vs.cursors.primary_mut().position = total;
1314 }
1315 }
1316 state.buffer.set_modified(false);
1318 state.editing_disabled = true;
1320 state.margins.configure_for_line_numbers(false);
1321 }
1322 }
1323 }
1324
1325 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
1327 for (buffer_id, metadata) in &self.buffer_metadata {
1329 if let Some(file_path) = metadata.file_path() {
1330 if file_path == path {
1331 return Ok(*buffer_id);
1332 }
1333 }
1334 }
1335
1336 self.open_file(path).map_err(WorkspaceError::Io)
1338 }
1339
1340 #[allow(clippy::too_many_arguments)]
1342 fn restore_split_node(
1343 &mut self,
1344 node: &SerializedSplitNode,
1345 path_to_buffer: &HashMap<PathBuf, BufferId>,
1346 terminal_buffers: &HashMap<usize, BufferId>,
1347 unnamed_buffers: &HashMap<String, BufferId>,
1348 split_states: &HashMap<usize, SerializedSplitViewState>,
1349 split_id_map: &mut HashMap<usize, SplitId>,
1350 is_first_leaf: bool,
1351 ) {
1352 match node {
1353 SerializedSplitNode::Leaf {
1354 file_path,
1355 split_id,
1356 label,
1357 unnamed_recovery_id,
1358 role,
1359 } => {
1360 let buffer_id = file_path
1362 .as_ref()
1363 .and_then(|p| path_to_buffer.get(p).copied())
1364 .or_else(|| {
1365 unnamed_recovery_id
1366 .as_ref()
1367 .and_then(|id| unnamed_buffers.get(id).copied())
1368 })
1369 .unwrap_or(self.active_buffer());
1370
1371 let current_leaf_id = if is_first_leaf {
1372 let leaf_id = self.split_manager.active_split();
1374 self.set_pane_buffer(leaf_id, buffer_id);
1375 leaf_id
1376 } else {
1377 self.split_manager.active_split()
1379 };
1380
1381 split_id_map.insert(*split_id, current_leaf_id.into());
1383
1384 if let Some(label) = label {
1386 self.split_manager.set_label(current_leaf_id, label.clone());
1387 }
1388
1389 if let Some(role) = role {
1392 self.split_manager.clear_role(*role);
1393 self.split_manager
1394 .set_leaf_role(current_leaf_id, Some(*role));
1395 }
1396
1397 self.restore_split_view_state(
1399 current_leaf_id,
1400 *split_id,
1401 split_states,
1402 path_to_buffer,
1403 terminal_buffers,
1404 unnamed_buffers,
1405 );
1406 }
1407 SerializedSplitNode::Terminal {
1408 terminal_index,
1409 split_id,
1410 label,
1411 role,
1412 } => {
1413 let buffer_id = terminal_buffers
1414 .get(terminal_index)
1415 .copied()
1416 .unwrap_or(self.active_buffer());
1417
1418 let current_leaf_id = if is_first_leaf {
1419 let leaf_id = self.split_manager.active_split();
1420 self.set_pane_buffer(leaf_id, buffer_id);
1421 leaf_id
1422 } else {
1423 self.split_manager.active_split()
1424 };
1425
1426 split_id_map.insert(*split_id, current_leaf_id.into());
1427
1428 if let Some(label) = label {
1430 self.split_manager.set_label(current_leaf_id, label.clone());
1431 }
1432
1433 if let Some(role) = role {
1436 self.split_manager.clear_role(*role);
1437 self.split_manager
1438 .set_leaf_role(current_leaf_id, Some(*role));
1439 }
1440
1441 self.split_manager
1442 .set_split_buffer(current_leaf_id, buffer_id);
1443
1444 self.restore_split_view_state(
1445 current_leaf_id,
1446 *split_id,
1447 split_states,
1448 path_to_buffer,
1449 terminal_buffers,
1450 unnamed_buffers,
1451 );
1452 }
1453 SerializedSplitNode::Split {
1454 direction,
1455 first,
1456 second,
1457 ratio,
1458 split_id,
1459 } => {
1460 self.restore_split_node(
1462 first,
1463 path_to_buffer,
1464 terminal_buffers,
1465 unnamed_buffers,
1466 split_states,
1467 split_id_map,
1468 is_first_leaf,
1469 );
1470
1471 let second_buffer_id = get_first_leaf_buffer(
1473 second,
1474 path_to_buffer,
1475 terminal_buffers,
1476 unnamed_buffers,
1477 )
1478 .unwrap_or(self.active_buffer());
1479
1480 let split_direction = match direction {
1482 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1483 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1484 };
1485
1486 match self
1488 .split_manager
1489 .split_active(split_direction, second_buffer_id, *ratio)
1490 {
1491 Ok(new_leaf_id) => {
1492 let mut view_state = SplitViewState::with_buffer(
1494 self.terminal_width,
1495 self.terminal_height,
1496 second_buffer_id,
1497 );
1498 view_state.apply_config_defaults(
1499 self.config.editor.line_numbers,
1500 self.config.editor.highlight_current_line,
1501 self.resolve_line_wrap_for_buffer(second_buffer_id),
1502 self.config.editor.wrap_indent,
1503 self.resolve_wrap_column_for_buffer(second_buffer_id),
1504 self.config.editor.rulers.clone(),
1505 );
1506 self.split_view_states.insert(new_leaf_id, view_state);
1507
1508 split_id_map.insert(*split_id, new_leaf_id.into());
1510
1511 self.restore_split_node(
1513 second,
1514 path_to_buffer,
1515 terminal_buffers,
1516 unnamed_buffers,
1517 split_states,
1518 split_id_map,
1519 false,
1520 );
1521 }
1522 Err(e) => {
1523 tracing::error!("Failed to create split during workspace restore: {}", e);
1524 }
1525 }
1526 }
1527 }
1528 }
1529
1530 fn restore_split_view_state(
1532 &mut self,
1533 current_split_id: LeafId,
1534 saved_split_id: usize,
1535 split_states: &HashMap<usize, SerializedSplitViewState>,
1536 path_to_buffer: &HashMap<PathBuf, BufferId>,
1537 terminal_buffers: &HashMap<usize, BufferId>,
1538 unnamed_buffers: &HashMap<String, BufferId>,
1539 ) {
1540 let Some(split_state) = split_states.get(&saved_split_id) else {
1542 return;
1543 };
1544
1545 let Some(view_state) = self.split_view_states.get_mut(¤t_split_id) else {
1546 return;
1547 };
1548
1549 let mut active_buffer_id: Option<BufferId> = None;
1550
1551 if !split_state.open_tabs.is_empty() {
1552 view_state.open_buffers.clear();
1555
1556 for tab in &split_state.open_tabs {
1557 match tab {
1558 SerializedTabRef::File(rel_path) => {
1559 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1560 if !view_state.has_buffer(buffer_id) {
1561 view_state.add_buffer(buffer_id);
1562 }
1563 view_state.ensure_buffer_state(buffer_id);
1565 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1566 view_state
1567 .buffer_state_mut(buffer_id)
1568 .unwrap()
1569 .viewport
1570 .line_wrap_enabled = false;
1571 }
1572 }
1573 }
1574 SerializedTabRef::Terminal(index) => {
1575 if let Some(&buffer_id) = terminal_buffers.get(index) {
1576 if !view_state.has_buffer(buffer_id) {
1577 view_state.add_buffer(buffer_id);
1578 }
1579 view_state
1580 .ensure_buffer_state(buffer_id)
1581 .viewport
1582 .line_wrap_enabled = false;
1583 }
1584 }
1585 SerializedTabRef::Unnamed(recovery_id) => {
1586 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1587 if !view_state.has_buffer(buffer_id) {
1588 view_state.add_buffer(buffer_id);
1589 }
1590 view_state.ensure_buffer_state(buffer_id);
1591 }
1592 }
1593 }
1594 }
1595
1596 if view_state.open_buffers.is_empty() {
1601 if let Some(buf) = self.split_manager.buffer_for_split(current_split_id) {
1602 view_state.add_buffer(buf);
1603 view_state.ensure_buffer_state(buf);
1604 }
1605 }
1606
1607 if let Some(active_idx) = split_state.active_tab_index {
1608 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1609 active_buffer_id = match tab {
1610 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1611 SerializedTabRef::Terminal(index) => terminal_buffers.get(index).copied(),
1612 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1613 };
1614 }
1615 }
1616 } else {
1617 for rel_path in &split_state.open_files {
1619 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1620 if !view_state.has_buffer(buffer_id) {
1621 view_state.add_buffer(buffer_id);
1622 }
1623 view_state.ensure_buffer_state(buffer_id);
1624 }
1625 }
1626
1627 let active_file_path = split_state.open_files.get(split_state.active_file_index);
1628 active_buffer_id =
1629 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1630 }
1631
1632 for (rel_path, file_state) in &split_state.file_states {
1634 let rel_str = rel_path.to_string_lossy();
1636 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1637 match unnamed_buffers.get(recovery_id).copied() {
1638 Some(id) => id,
1639 None => continue,
1640 }
1641 } else {
1642 match path_to_buffer.get(rel_path).copied() {
1643 Some(id) => id,
1644 None => continue,
1645 }
1646 };
1647 let max_pos = self
1648 .buffers
1649 .get(&buffer_id)
1650 .map(|b| b.buffer.len())
1651 .unwrap_or(0);
1652
1653 let buf_state = view_state.ensure_buffer_state(buffer_id);
1655
1656 let cursor_pos = file_state.cursor.position.min(max_pos);
1657 buf_state.cursors.primary_mut().position = cursor_pos;
1658 buf_state.cursors.primary_mut().anchor =
1659 file_state.cursor.anchor.map(|a| a.min(max_pos));
1660 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1661
1662 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1663 buf_state.viewport.top_view_line_offset = file_state.scroll.top_view_line_offset;
1664 buf_state.viewport.left_column = file_state.scroll.left_column;
1665 buf_state.viewport.set_skip_resize_sync();
1666
1667 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1675 super::navigation::reconcile_restored_buffer_view(buf_state, &mut state.buffer);
1676 }
1677
1678 buf_state.view_mode = match file_state.view_mode {
1680 SerializedViewMode::Source => ViewMode::Source,
1681 SerializedViewMode::PageView => ViewMode::PageView,
1682 };
1683 buf_state.compose_width = file_state.compose_width;
1684 buf_state.plugin_state = file_state.plugin_state.clone();
1685 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1686 buf_state.folds.clear(&mut state.marker_list);
1687 for fold in &file_state.folds {
1688 let Some(resolved_header) = resolve_fold_header_line(
1695 &state.buffer,
1696 fold.header_line,
1697 fold.header_text.as_deref(),
1698 ) else {
1699 tracing::debug!(
1700 "Dropping stale fold: header_line={} no longer matches stored \
1701 header_text after external edit",
1702 fold.header_line,
1703 );
1704 continue;
1705 };
1706
1707 let shift = resolved_header as i64 - fold.header_line as i64;
1709 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1710 let start_line = resolved_header.saturating_add(1);
1711 let end_line = adjusted_end;
1712 if start_line > end_line {
1713 continue;
1714 }
1715 let Some(start_byte) = state.buffer.line_start_offset(start_line) else {
1716 continue;
1717 };
1718 let end_byte = state
1719 .buffer
1720 .line_start_offset(end_line.saturating_add(1))
1721 .unwrap_or_else(|| state.buffer.len());
1722 buf_state.folds.add(
1723 &mut state.marker_list,
1724 start_byte,
1725 end_byte,
1726 fold.placeholder.clone(),
1727 );
1728 }
1729 }
1730
1731 tracing::trace!(
1732 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1733 rel_path,
1734 cursor_pos,
1735 buf_state.viewport.top_byte,
1736 buf_state.view_mode,
1737 );
1738 }
1739
1740 let restored_view_mode = match split_state.view_mode {
1743 SerializedViewMode::Source => ViewMode::Source,
1744 SerializedViewMode::PageView => ViewMode::PageView,
1745 };
1746
1747 if let Some(active_id) = active_buffer_id {
1748 view_state.switch_buffer(active_id);
1750
1751 let active_has_file_state = split_state
1753 .file_states
1754 .keys()
1755 .any(|rel_path| path_to_buffer.get(rel_path).copied() == Some(active_id));
1756 if !active_has_file_state {
1757 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1758 view_state.active_state_mut().compose_width = split_state.compose_width;
1759 }
1760
1761 self.split_manager
1765 .set_split_buffer(current_split_id, active_id);
1766 }
1767 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1768 }
1769}
1770
1771fn get_first_leaf_buffer(
1773 node: &SerializedSplitNode,
1774 path_to_buffer: &HashMap<PathBuf, BufferId>,
1775 terminal_buffers: &HashMap<usize, BufferId>,
1776 unnamed_buffers: &HashMap<String, BufferId>,
1777) -> Option<BufferId> {
1778 match node {
1779 SerializedSplitNode::Leaf {
1780 file_path,
1781 unnamed_recovery_id,
1782 ..
1783 } => file_path
1784 .as_ref()
1785 .and_then(|p| path_to_buffer.get(p).copied())
1786 .or_else(|| {
1787 unnamed_recovery_id
1788 .as_ref()
1789 .and_then(|id| unnamed_buffers.get(id).copied())
1790 }),
1791 SerializedSplitNode::Terminal { terminal_index, .. } => {
1792 terminal_buffers.get(terminal_index).copied()
1793 }
1794 SerializedSplitNode::Split { first, .. } => {
1795 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
1796 }
1797 }
1798}
1799
1800fn serialize_split_node(
1805 node: &SplitNode,
1806 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1807 working_dir: &Path,
1808 terminal_buffers: &HashMap<BufferId, TerminalId>,
1809 terminal_indices: &HashMap<TerminalId, usize>,
1810 split_labels: &HashMap<SplitId, String>,
1811) -> SerializedSplitNode {
1812 serialize_split_node_pruned(
1813 node,
1814 buffer_metadata,
1815 working_dir,
1816 terminal_buffers,
1817 terminal_indices,
1818 split_labels,
1819 )
1820 .unwrap_or({
1821 SerializedSplitNode::Leaf {
1824 file_path: None,
1825 split_id: 0,
1826 label: None,
1827 unnamed_recovery_id: None,
1828 role: None,
1829 }
1830 })
1831}
1832
1833fn serialize_split_node_pruned(
1840 node: &SplitNode,
1841 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1842 working_dir: &Path,
1843 terminal_buffers: &HashMap<BufferId, TerminalId>,
1844 terminal_indices: &HashMap<TerminalId, usize>,
1845 split_labels: &HashMap<SplitId, String>,
1846) -> Option<SerializedSplitNode> {
1847 match node {
1848 SplitNode::Grouped { layout, .. } => {
1849 serialize_split_node_pruned(
1853 layout,
1854 buffer_metadata,
1855 working_dir,
1856 terminal_buffers,
1857 terminal_indices,
1858 split_labels,
1859 )
1860 }
1861 SplitNode::Leaf {
1862 buffer_id,
1863 split_id,
1864 role,
1865 } => {
1866 let raw_split_id: SplitId = (*split_id).into();
1867 let label = split_labels.get(&raw_split_id).cloned();
1868 let role = *role;
1869
1870 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1871 if let Some(index) = terminal_indices.get(terminal_id) {
1872 return Some(SerializedSplitNode::Terminal {
1873 terminal_index: *index,
1874 split_id: raw_split_id.0,
1875 label,
1876 role,
1877 });
1878 }
1879 }
1880
1881 let meta = buffer_metadata.get(buffer_id);
1882
1883 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
1887 return None;
1888 }
1889
1890 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
1891 if abs_path.as_os_str().is_empty() {
1892 None } else {
1894 abs_path
1895 .strip_prefix(working_dir)
1896 .ok()
1897 .map(|p| p.to_path_buf())
1898 }
1899 });
1900
1901 let unnamed_recovery_id = if file_path.is_none() {
1904 meta.and_then(|m| m.recovery_id.clone())
1905 } else {
1906 None
1907 };
1908
1909 Some(SerializedSplitNode::Leaf {
1910 file_path,
1911 split_id: raw_split_id.0,
1912 label,
1913 unnamed_recovery_id,
1914 role,
1915 })
1916 }
1917 SplitNode::Split {
1918 direction,
1919 first,
1920 second,
1921 ratio,
1922 split_id,
1923 ..
1924 } => {
1925 let raw_split_id: SplitId = (*split_id).into();
1926 let first = serialize_split_node_pruned(
1927 first,
1928 buffer_metadata,
1929 working_dir,
1930 terminal_buffers,
1931 terminal_indices,
1932 split_labels,
1933 );
1934 let second = serialize_split_node_pruned(
1935 second,
1936 buffer_metadata,
1937 working_dir,
1938 terminal_buffers,
1939 terminal_indices,
1940 split_labels,
1941 );
1942 match (first, second) {
1943 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
1944 direction: match direction {
1945 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
1946 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
1947 },
1948 first: Box::new(f),
1949 second: Box::new(s),
1950 ratio: *ratio,
1951 split_id: raw_split_id.0,
1952 }),
1953 (Some(only), None) | (None, Some(only)) => Some(only),
1956 (None, None) => None,
1957 }
1958 }
1959 }
1960}
1961
1962fn serialize_split_view_state(
1963 view_state: &crate::view::split::SplitViewState,
1964 buffers: &HashMap<BufferId, EditorState>,
1965 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1966 working_dir: &Path,
1967 active_buffer: Option<BufferId>,
1968 terminal_buffers: &HashMap<BufferId, TerminalId>,
1969 terminal_indices: &HashMap<TerminalId, usize>,
1970) -> SerializedSplitViewState {
1971 let mut open_tabs = Vec::new();
1972 let mut open_files = Vec::new();
1973 let mut active_tab_index = None;
1974
1975 for buffer_id in view_state.buffer_tab_ids() {
1977 let buffer_id = &buffer_id;
1978 let tab_index = open_tabs.len();
1979 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1980 if let Some(idx) = terminal_indices.get(terminal_id) {
1981 open_tabs.push(SerializedTabRef::Terminal(*idx));
1982 if Some(*buffer_id) == active_buffer {
1983 active_tab_index = Some(tab_index);
1984 }
1985 continue;
1986 }
1987 }
1988
1989 if let Some(meta) = buffer_metadata.get(buffer_id) {
1990 if let Some(abs_path) = meta.file_path() {
1991 if abs_path.as_os_str().is_empty() {
1992 if let Some(ref recovery_id) = meta.recovery_id {
1994 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
1995 if Some(*buffer_id) == active_buffer {
1996 active_tab_index = Some(tab_index);
1997 }
1998 }
1999 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2000 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2001 open_files.push(rel_path.to_path_buf());
2002 if Some(*buffer_id) == active_buffer {
2003 active_tab_index = Some(tab_index);
2004 }
2005 } else {
2006 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2008 if Some(*buffer_id) == active_buffer {
2009 active_tab_index = Some(tab_index);
2010 }
2011 }
2012 }
2013 }
2014 }
2015
2016 let active_file_index = active_tab_index
2018 .and_then(|idx| open_tabs.get(idx))
2019 .and_then(|tab| match tab {
2020 SerializedTabRef::File(path) => {
2021 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2022 }
2023 _ => None,
2024 })
2025 .unwrap_or(0);
2026
2027 let mut file_states = HashMap::new();
2029 for (buffer_id, buf_state) in &view_state.keyed_states {
2030 let Some(meta) = buffer_metadata.get(buffer_id) else {
2031 continue;
2032 };
2033 let Some(abs_path) = meta.file_path() else {
2034 continue;
2035 };
2036
2037 let state_key = if abs_path.as_os_str().is_empty() {
2039 if let Some(ref recovery_id) = meta.recovery_id {
2041 PathBuf::from(format!("__unnamed__{}", recovery_id))
2042 } else {
2043 continue;
2044 }
2045 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2046 rp.to_path_buf()
2047 } else {
2048 abs_path.to_path_buf()
2050 };
2051
2052 let primary_cursor = buf_state.cursors.primary();
2053 let folds = buffers
2054 .get(buffer_id)
2055 .map(|state| {
2056 buf_state
2057 .folds
2058 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2059 .into_iter()
2060 .map(|range| SerializedFoldRange {
2061 header_line: range.header_line,
2062 end_line: range.end_line,
2063 placeholder: range.placeholder,
2064 header_text: range.header_text,
2065 })
2066 .collect::<Vec<_>>()
2067 })
2068 .unwrap_or_default();
2069
2070 file_states.insert(
2071 state_key,
2072 SerializedFileState {
2073 cursor: SerializedCursor {
2074 position: primary_cursor.position,
2075 anchor: primary_cursor.anchor,
2076 sticky_column: primary_cursor.sticky_column,
2077 },
2078 additional_cursors: buf_state
2079 .cursors
2080 .iter()
2081 .skip(1) .map(|(_, cursor)| SerializedCursor {
2083 position: cursor.position,
2084 anchor: cursor.anchor,
2085 sticky_column: cursor.sticky_column,
2086 })
2087 .collect(),
2088 scroll: SerializedScroll {
2089 top_byte: buf_state.viewport.top_byte,
2090 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2091 left_column: buf_state.viewport.left_column,
2092 },
2093 view_mode: match buf_state.view_mode {
2094 ViewMode::Source => SerializedViewMode::Source,
2095 ViewMode::PageView => SerializedViewMode::PageView,
2096 },
2097 compose_width: buf_state.compose_width,
2098 plugin_state: buf_state.plugin_state.clone(),
2099 folds,
2100 },
2101 );
2102 }
2103
2104 let active_view_mode = active_buffer
2106 .and_then(|id| view_state.keyed_states.get(&id))
2107 .map(|bs| match bs.view_mode {
2108 ViewMode::Source => SerializedViewMode::Source,
2109 ViewMode::PageView => SerializedViewMode::PageView,
2110 })
2111 .unwrap_or(SerializedViewMode::Source);
2112 let active_compose_width = active_buffer
2113 .and_then(|id| view_state.keyed_states.get(&id))
2114 .and_then(|bs| bs.compose_width);
2115
2116 SerializedSplitViewState {
2117 open_tabs,
2118 active_tab_index,
2119 open_files,
2120 active_file_index,
2121 file_states,
2122 tab_scroll_offset: view_state.tab_scroll_offset,
2123 view_mode: active_view_mode,
2124 compose_width: active_compose_width,
2125 }
2126}
2127
2128fn serialize_bookmarks(
2129 bookmarks: &BookmarkState,
2130 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2131 working_dir: &Path,
2132) -> HashMap<char, SerializedBookmark> {
2133 bookmarks
2134 .iter()
2135 .filter_map(|(key, bookmark)| {
2136 buffer_metadata
2137 .get(&bookmark.buffer_id)
2138 .and_then(|meta| meta.file_path())
2139 .and_then(|abs_path| {
2140 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2141 (
2142 key,
2143 SerializedBookmark {
2144 file_path: rel_path.to_path_buf(),
2145 position: bookmark.position,
2146 },
2147 )
2148 })
2149 })
2150 })
2151 .collect()
2152}
2153
2154fn collect_file_paths_from_states(
2156 split_states: &HashMap<usize, SerializedSplitViewState>,
2157) -> Vec<PathBuf> {
2158 let mut paths = Vec::new();
2159 for state in split_states.values() {
2160 if !state.open_tabs.is_empty() {
2161 for tab in &state.open_tabs {
2162 if let SerializedTabRef::File(path) = tab {
2163 if !paths.contains(path) {
2164 paths.push(path.clone());
2165 }
2166 }
2167 }
2168 } else {
2169 for path in &state.open_files {
2170 if !paths.contains(path) {
2171 paths.push(path.clone());
2172 }
2173 }
2174 }
2175 }
2176 paths
2177}
2178
2179fn get_expanded_dirs(
2181 explorer: &crate::view::file_tree::FileTreeView,
2182 working_dir: &Path,
2183) -> Vec<PathBuf> {
2184 let mut expanded = Vec::new();
2185 let tree = explorer.tree();
2186
2187 for node in tree.all_nodes() {
2189 if node.is_expanded() && node.is_dir() {
2190 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2192 expanded.push(rel_path.to_path_buf());
2193 }
2194 }
2195 }
2196
2197 expanded
2198}