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 {
158 self.active_window().capture_workspace()
159 }
160
161 pub fn plugin_global_state(
167 &self,
168 ) -> &std::collections::HashMap<String, std::collections::HashMap<String, serde_json::Value>>
169 {
170 &self.plugin_global_state
171 }
172
173 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
176 self.save_workspace_for(self.active_window)
177 }
178
179 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
184 self.restore_workspace_for(self.active_window)
185 }
186
187 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
193 if !self.config.editor.hot_exit {
194 return Ok(0);
195 }
196
197 let entries = self.recovery_service.lock().unwrap().list_recoverable()?;
198 if entries.is_empty() {
199 return Ok(0);
200 }
201
202 let buffer_files: Vec<_> = self
204 .buffers()
205 .iter()
206 .filter_map(|(buffer_id, state)| {
207 let path = state.buffer.file_path()?.to_path_buf();
208 if path.as_os_str().is_empty() {
209 return None; }
211 Some((*buffer_id, path))
212 })
213 .collect();
214
215 let mut recovered = 0;
216 for (buffer_id, file_path) in buffer_files {
217 let recovery_id = self
218 .recovery_service
219 .lock()
220 .unwrap()
221 .get_buffer_id(Some(&file_path));
222 let entry = entries.iter().find(|e| e.id == recovery_id);
223 if let Some(entry) = entry {
224 let loaded = self.recovery_service.lock().unwrap().load_recovery(entry);
225 match loaded {
226 Ok(crate::services::recovery::RecoveryResult::Recovered {
227 content, ..
228 }) => {
229 let mut mutated = false;
230 if let Some(state) = self
231 .windows
232 .get_mut(&self.active_window)
233 .map(|w| &mut w.buffers)
234 .expect("active window present")
235 .get_mut(&buffer_id)
236 {
237 let current_len = state.buffer.total_bytes();
238 let text = String::from_utf8_lossy(&content).into_owned();
239 let current = state.buffer.get_text_range_mut(0, current_len).ok();
240 let current_text = current
241 .as_ref()
242 .map(|b| String::from_utf8_lossy(b).into_owned());
243 if current_text.as_deref() != Some(&text) {
244 state.buffer.delete(0..current_len);
245 state.buffer.insert(0, &text);
246 state.buffer.set_modified(true);
247 state.buffer.set_recovery_pending(false);
248 if let Some(log) =
251 self.active_window_mut().event_logs.get_mut(&buffer_id)
252 {
253 log.clear_saved_position();
254 }
255 mutated = true;
256 recovered += 1;
257 tracing::info!(
258 "Restored unsaved changes for {:?} from hot exit recovery",
259 file_path
260 );
261 }
262 }
263 if mutated {
264 self.sync_lsp_after_recovery_replay(buffer_id);
265 }
266 }
267 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
268 chunks,
269 ..
270 }) => {
271 let mut mutated = false;
272 if let Some(state) = self
273 .windows
274 .get_mut(&self.active_window)
275 .map(|w| &mut w.buffers)
276 .expect("active window present")
277 .get_mut(&buffer_id)
278 {
279 for chunk in chunks.into_iter().rev() {
280 let text = String::from_utf8_lossy(&chunk.content).into_owned();
281 if chunk.original_len > 0 {
282 state
283 .buffer
284 .delete(chunk.offset..chunk.offset + chunk.original_len);
285 }
286 state.buffer.insert(chunk.offset, &text);
287 }
288 state.buffer.set_modified(true);
289 state.buffer.set_recovery_pending(false);
290 if let Some(log) =
293 self.active_window_mut().event_logs.get_mut(&buffer_id)
294 {
295 log.clear_saved_position();
296 }
297 mutated = true;
298 recovered += 1;
299 tracing::info!(
300 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
301 file_path
302 );
303 }
304 if mutated {
305 self.sync_lsp_after_recovery_replay(buffer_id);
306 }
307 }
308 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
309 original_path,
310 ..
311 }) => {
312 let name = original_path
313 .file_name()
314 .unwrap_or_default()
315 .to_string_lossy();
316 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
317 self.set_status_message(format!(
318 "{} changed on disk; unsaved changes not restored",
319 name
320 ));
321 }
322 Ok(_) => {} Err(e) => {
324 tracing::debug!(
325 "Failed to load hot exit recovery for {:?}: {}",
326 file_path,
327 e
328 );
329 }
330 }
331 }
332 }
333
334 Ok(recovered)
335 }
336
337 fn restore_config_overrides(&mut self, overrides: &WorkspaceConfigOverrides) {
342 if let Some(line_numbers) = overrides.line_numbers {
343 self.config_mut().editor.line_numbers = line_numbers;
344 }
345 if let Some(relative_line_numbers) = overrides.relative_line_numbers {
346 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
347 }
348 if let Some(line_wrap) = overrides.line_wrap {
349 self.config_mut().editor.line_wrap = line_wrap;
350 }
351 if let Some(syntax_highlighting) = overrides.syntax_highlighting {
352 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
353 }
354 if let Some(enable_inlay_hints) = overrides.enable_inlay_hints {
355 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
356 }
357 }
362
363 pub fn save_workspace_for(&mut self, id: fresh_core::WindowId) -> Result<(), WorkspaceError> {
368 let Some(win) = self.windows.get(&id) else {
369 return Ok(());
370 };
371
372 win.sync_terminal_backing_files();
375 win.save_all_global_file_states();
376
377 let workspace = win.capture_workspace();
378
379 if workspace.has_no_real_content() && win.has_any_virtual_buffer() {
384 let root = win.root.clone();
385 let on_disk = if let Some(ref session_name) = self.session_name {
386 Workspace::load_session(session_name, &root).ok().flatten()
387 } else {
388 Workspace::load(&root).ok().flatten()
389 };
390 if let Some(existing) = on_disk {
391 if !existing.has_no_preservable_content() {
392 tracing::info!(
393 "Skipping workspace save: only virtual buffers are open, \
394 on-disk workspace already has preservable file content"
395 );
396 return Ok(());
397 }
398 }
399 }
400
401 if let Some(ref session_name) = self.session_name {
403 workspace.save_session(session_name)
404 } else {
405 workspace.save()
406 }
407 }
408
409 pub fn restore_workspace_for(
421 &mut self,
422 id: fresh_core::WindowId,
423 ) -> Result<bool, WorkspaceError> {
424 let Some(root) = self.windows.get(&id).map(|w| w.root.clone()) else {
425 return Ok(false);
426 };
427
428 let workspace = if let Some(ref session_name) = self.session_name {
429 Workspace::load_session(session_name, &root)?
430 } else {
431 Workspace::load(&root)?
432 };
433 let Some(workspace) = workspace else {
434 tracing::debug!("No workspace found for {:?}", root);
435 return Ok(false);
436 };
437 tracing::info!("Found workspace for {:?}, applying...", root);
438
439 self.restore_config_overrides(&workspace.config_overrides);
441 let populated = self
448 .windows
449 .get(&id)
450 .map(|w| w.buffers.splits().is_some() && w.buffers.len() > 0)
451 .unwrap_or(false);
452
453 let session = self.session_name.clone();
454 if populated {
455 let win = self
458 .windows
459 .get_mut(&id)
460 .expect("window present for restore");
461 win.apply_workspace_layout(&workspace, session.as_deref());
462 win.authority_spec = workspace.authority_spec.clone();
466 } else {
467 let old = self
473 .windows
474 .remove(&id)
475 .expect("window present for restore");
476 let (label, root2, authority, resources, tw, th, pstate) = (
477 old.label,
478 old.root,
479 old.authority,
480 old.resources,
481 old.terminal_width,
482 old.terminal_height,
483 old.plugin_state,
484 );
485 let mut built = crate::app::window::Window::from_workspace(
486 id, label, root2, authority, resources, &workspace,
487 );
488 built.terminal_width = tw;
489 built.terminal_height = th;
490 built.plugin_state = pstate;
491 built.authority_spec = workspace.authority_spec.clone();
492 self.windows.insert(id, built);
493 }
494
495 if id == self.active_window {
499 #[cfg(feature = "plugins")]
500 {
501 let buffer_id = self.active_buffer();
502 self.update_plugin_state_snapshot();
503 tracing::debug!(
504 "Firing buffer_activated for active buffer {:?} after workspace restore",
505 buffer_id
506 );
507 self.plugin_manager.read().unwrap().run_hook(
508 "buffer_activated",
509 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
510 );
511 }
512 }
513
514 Ok(true)
515 }
516
517 pub fn save_all_windows_workspaces(&mut self) -> Result<(), WorkspaceError> {
524 let targets: Vec<fresh_core::WindowId> = self
525 .windows
526 .iter()
527 .filter(|(id, w)| {
532 w.buffers.splits().is_some() && !self.materialize_pending.contains(id)
533 })
534 .map(|(id, _)| *id)
535 .collect();
536
537 let mut first_err = None;
538 for id in targets {
539 if let Err(e) = self.save_workspace_for(id) {
540 tracing::warn!("Failed to save workspace for window {id}: {e}");
541 if first_err.is_none() {
542 first_err = Some(e);
543 }
544 }
545 }
546
547 match first_err {
548 Some(e) => Err(e),
549 None => Ok(()),
550 }
551 }
552
553 pub(crate) fn materialize_window(&mut self, id: fresh_core::WindowId) {
564 if !self.materialize_pending.remove(&id) {
565 return;
566 }
567 let saved_plugin_state = self.plugin_global_state.clone();
568 match self.restore_workspace_for(id) {
569 Ok(true) => tracing::debug!("Materialized window {id} from workspace"),
570 Ok(false) => {
571 tracing::trace!("No persisted workspace for window {id}; empty seed kept")
572 }
573 Err(e) => tracing::warn!("Failed to materialize window {id}: {e}"),
574 }
575 self.plugin_global_state = saved_plugin_state;
576 }
577
578 pub fn materialize_all_windows(&mut self) {
585 let pending: Vec<fresh_core::WindowId> = self.materialize_pending.iter().copied().collect();
586 for id in pending {
587 self.materialize_window(id);
588 }
589 }
590}
591
592impl crate::app::window::Window {
593 fn restore_terminals_from_workspace(
594 &mut self,
595 terminals: &[SerializedTerminalWorkspace],
596 ) -> HashMap<usize, BufferId> {
597 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
598 if terminals.is_empty() {
599 return terminal_buffer_map;
600 }
601 let __window_bridge = self.bridge.clone();
602 self.terminal_manager.set_async_bridge(__window_bridge);
603 for terminal in terminals {
604 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
605 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
606 self.terminal_mode_resume.insert(buffer_id);
615 }
616 }
617 terminal_buffer_map
618 }
619
620 fn restore_bookmarks_from_workspace(
622 &mut self,
623 bookmarks: &HashMap<char, SerializedBookmark>,
624 path_to_buffer: &HashMap<PathBuf, BufferId>,
625 ) {
626 for (key, bookmark) in bookmarks {
627 let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) else {
628 continue;
629 };
630 if let Some(buffer) = self.buffers.get(&buffer_id) {
631 let pos = bookmark.position.min(buffer.buffer.len());
632 self.bookmarks.set(
633 *key,
634 Bookmark {
635 buffer_id,
636 position: pos,
637 },
638 );
639 }
640 }
641 }
642
643 fn clean_orphaned_buffers(&mut self) {
646 let referenced: HashSet<BufferId> = self
647 .buffers
648 .splits()
649 .map(|(_, vs)| vs)
650 .expect("active window must have a populated split layout")
651 .values()
652 .flat_map(|vs| vs.buffer_tab_ids())
653 .collect();
654 let orphans: Vec<BufferId> = self
655 .buffers
656 .iter()
657 .filter(|(id, state)| {
658 !referenced.contains(id)
659 && state.buffer.file_path().is_none()
660 && !state.buffer.is_modified()
661 })
662 .map(|(id, _)| *id)
663 .collect();
664 for id in orphans {
665 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
666 self.buffers.remove(&id);
667 self.event_logs.remove(&id);
668 self.buffer_metadata.remove(&id);
669 }
670 }
671
672 fn log_restore_summary(&mut self, session_name: Option<&str>) {
675 tracing::debug!(
676 "Workspace restore complete: {} splits, {} buffers",
677 self.buffers
678 .splits()
679 .map(|(_, vs)| vs)
680 .expect("active window must have a populated split layout")
681 .len(),
682 self.buffers.len()
683 );
684 let restored_count = self.buffers.count_where(|id, _| {
685 self.buffer_metadata
686 .get(&id)
687 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
688 });
689 if restored_count == 0 {
690 return;
691 }
692 let msg = match session_name.map(|n| format!("session '{}'", n)) {
693 Some(label) => format!("Restored {} ({} buffer(s))", label, restored_count),
694 None => format!(
695 "Restored {} buffer(s) from previous session",
696 restored_count
697 ),
698 };
699 self.set_status_message(msg);
700 }
701
702 fn restore_terminal_from_workspace(
711 &mut self,
712 terminal: &SerializedTerminalWorkspace,
713 ) -> Option<BufferId> {
714 let terminals_root = self
716 .resources
717 .dir_context
718 .terminal_dir_for(self.root.as_path());
719 let log_path = if terminal.log_path.is_absolute() {
720 terminal.log_path.clone()
721 } else {
722 terminals_root.join(&terminal.log_path)
723 };
724 let backing_path = if terminal.backing_path.is_absolute() {
725 terminal.backing_path.clone()
726 } else {
727 terminals_root.join(&terminal.backing_path)
728 };
729
730 #[allow(clippy::let_underscore_must_use)]
732 let _ = self.authority().filesystem.create_dir_all(
733 log_path
734 .parent()
735 .or_else(|| backing_path.parent())
736 .unwrap_or(&terminals_root),
737 );
738
739 let predicted_id = self.terminal_manager.next_terminal_id();
741 self.terminal_log_files
742 .insert(predicted_id, log_path.clone());
743 self.terminal_backing_files
744 .insert(predicted_id, backing_path.clone());
745
746 let resume_argv = terminal
755 .agent_resume
756 .as_ref()
757 .map(|r| &r.argv)
758 .filter(|argv| !argv.is_empty() && self.resources.config.terminal.resume_agents);
759 let spawn_argv =
760 resume_argv.or_else(|| terminal.command.as_ref().filter(|argv| !argv.is_empty()));
761 let wrapper_for_spawn = match spawn_argv {
768 Some(argv) => self.authority().terminal_command(argv),
769 None => self.resolved_terminal_wrapper(),
770 };
771 let wrapper_for_spawn = self.apply_remote_terminal_env(wrapper_for_spawn);
772 let env_delta = self.terminal_env_delta(&wrapper_for_spawn);
773 let terminal_id = match self.terminal_manager.spawn(
774 terminal.cols,
775 terminal.rows,
776 terminal.cwd.clone(),
777 Some(log_path.clone()),
778 Some(backing_path.clone()),
779 wrapper_for_spawn,
780 env_delta,
781 ) {
782 Ok(id) => id,
783 Err(e) => {
784 tracing::warn!(
785 "Failed to restore terminal {}: {}",
786 terminal.terminal_index,
787 e
788 );
789 return None;
790 }
791 };
792
793 if terminal_id != predicted_id {
795 self.terminal_log_files
796 .insert(terminal_id, log_path.clone());
797 self.terminal_backing_files
798 .insert(terminal_id, backing_path.clone());
799 self.terminal_log_files.remove(&predicted_id);
800 self.terminal_backing_files.remove(&predicted_id);
801 }
802
803 if let Some(argv) = terminal.command.as_ref() {
807 self.terminal_commands.insert(terminal_id, argv.clone());
808 }
809 if let Some(resume) = terminal.agent_resume.as_ref() {
810 if !resume.argv.is_empty() {
811 self.terminal_resume_commands
812 .insert(terminal_id, resume.argv.clone());
813 }
814 }
815
816 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
818
819 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
822
823 Some(buffer_id)
824 }
825
826 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
831 if !backing_path.exists() {
833 return;
834 }
835
836 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
837 if let Ok(new_state) = EditorState::from_file_with_languages(
838 backing_path,
839 self.terminal_width,
840 self.terminal_height,
841 large_file_threshold,
842 &self.resources.grammar_registry,
843 &self.resources.config.languages,
844 std::sync::Arc::clone(&self.authority().filesystem),
845 ) {
846 self.install_terminal_buffer_state(buffer_id, new_state);
847 }
848 }
849
850 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
852 for (buffer_id, metadata) in &self.buffer_metadata {
854 if let Some(file_path) = metadata.file_path() {
855 if file_path == path {
856 return Ok(*buffer_id);
857 }
858 }
859 }
860
861 self.open_file_no_focus(path).map_err(WorkspaceError::Io)
863 }
864
865 #[allow(clippy::too_many_arguments)]
867 fn restore_split_node(
868 &mut self,
869 node: &SerializedSplitNode,
870 path_to_buffer: &HashMap<PathBuf, BufferId>,
871 terminal_buffers: &HashMap<usize, BufferId>,
872 unnamed_buffers: &HashMap<String, BufferId>,
873 split_states: &HashMap<usize, SerializedSplitViewState>,
874 split_id_map: &mut HashMap<usize, SplitId>,
875 is_first_leaf: bool,
876 ) {
877 match node {
878 SerializedSplitNode::Leaf {
879 file_path,
880 split_id,
881 label,
882 unnamed_recovery_id,
883 role,
884 } => {
885 let buffer_id = file_path
887 .as_ref()
888 .and_then(|p| path_to_buffer.get(p).copied())
889 .or_else(|| {
890 unnamed_recovery_id
891 .as_ref()
892 .and_then(|id| unnamed_buffers.get(id).copied())
893 })
894 .unwrap_or(self.active_buffer());
895
896 let current_leaf_id = if is_first_leaf {
897 let leaf_id = self
899 .buffers
900 .splits()
901 .map(|(mgr, _)| mgr)
902 .expect("active window must have a populated split layout")
903 .active_split();
904 self.set_pane_buffer(leaf_id, buffer_id);
905 leaf_id
906 } else {
907 self.buffers
909 .splits()
910 .map(|(mgr, _)| mgr)
911 .expect("active window must have a populated split layout")
912 .active_split()
913 };
914
915 split_id_map.insert(*split_id, current_leaf_id.into());
917
918 if let Some(label) = label {
920 self.buffers
921 .split_manager_mut()
922 .expect("active window must have a populated split layout")
923 .set_label(current_leaf_id, label.clone());
924 }
925
926 if let Some(role) = role {
929 self.buffers
930 .split_manager_mut()
931 .expect("active window must have a populated split layout")
932 .clear_role(*role);
933 self.buffers
934 .split_manager_mut()
935 .expect("active window must have a populated split layout")
936 .set_leaf_role(current_leaf_id, Some(*role));
937 }
938
939 self.restore_split_view_state(
941 current_leaf_id,
942 *split_id,
943 split_states,
944 path_to_buffer,
945 terminal_buffers,
946 unnamed_buffers,
947 );
948 }
949 SerializedSplitNode::Terminal {
950 terminal_index,
951 split_id,
952 label,
953 role,
954 } => {
955 let buffer_id = terminal_buffers
956 .get(terminal_index)
957 .copied()
958 .unwrap_or(self.active_buffer());
959
960 let current_leaf_id = if is_first_leaf {
961 let leaf_id = self
962 .buffers
963 .splits()
964 .map(|(mgr, _)| mgr)
965 .expect("active window must have a populated split layout")
966 .active_split();
967 self.set_pane_buffer(leaf_id, buffer_id);
968 leaf_id
969 } else {
970 self.buffers
971 .splits()
972 .map(|(mgr, _)| mgr)
973 .expect("active window must have a populated split layout")
974 .active_split()
975 };
976
977 split_id_map.insert(*split_id, current_leaf_id.into());
978
979 if let Some(label) = label {
981 self.buffers
982 .split_manager_mut()
983 .expect("active window must have a populated split layout")
984 .set_label(current_leaf_id, label.clone());
985 }
986
987 if let Some(role) = role {
990 self.buffers
991 .split_manager_mut()
992 .expect("active window must have a populated split layout")
993 .clear_role(*role);
994 self.buffers
995 .split_manager_mut()
996 .expect("active window must have a populated split layout")
997 .set_leaf_role(current_leaf_id, Some(*role));
998 }
999
1000 self.buffers
1001 .split_manager_mut()
1002 .expect("active window must have a populated split layout")
1003 .set_split_buffer(current_leaf_id, buffer_id);
1004
1005 self.restore_split_view_state(
1006 current_leaf_id,
1007 *split_id,
1008 split_states,
1009 path_to_buffer,
1010 terminal_buffers,
1011 unnamed_buffers,
1012 );
1013 }
1014 SerializedSplitNode::Split {
1015 direction,
1016 first,
1017 second,
1018 ratio,
1019 split_id,
1020 } => {
1021 self.restore_split_node(
1023 first,
1024 path_to_buffer,
1025 terminal_buffers,
1026 unnamed_buffers,
1027 split_states,
1028 split_id_map,
1029 is_first_leaf,
1030 );
1031
1032 let second_buffer_id = get_first_leaf_buffer(
1034 second,
1035 path_to_buffer,
1036 terminal_buffers,
1037 unnamed_buffers,
1038 )
1039 .unwrap_or(self.active_buffer());
1040
1041 let split_direction = match direction {
1043 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1044 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1045 };
1046
1047 match self
1049 .buffers
1050 .split_manager_mut()
1051 .expect("active window must have a populated split layout")
1052 .split_active(split_direction, second_buffer_id, *ratio)
1053 {
1054 Ok(new_leaf_id) => {
1055 let mut view_state = SplitViewState::with_buffer(
1057 self.terminal_width,
1058 self.terminal_height,
1059 second_buffer_id,
1060 );
1061 view_state.apply_config_defaults(
1062 self.resources.config.editor.line_numbers,
1063 self.resources.config.editor.highlight_current_line,
1064 self.resolve_line_wrap_for_buffer(second_buffer_id),
1065 self.resources.config.editor.wrap_indent,
1066 self.resolve_wrap_column_for_buffer(second_buffer_id),
1067 self.resources.config.editor.rulers.clone(),
1068 self.resources.config.editor.scroll_offset,
1069 );
1070 self.buffers
1071 .split_view_states_mut()
1072 .expect("active window must have a populated split layout")
1073 .insert(new_leaf_id, view_state);
1074
1075 split_id_map.insert(*split_id, new_leaf_id.into());
1077
1078 self.restore_split_node(
1080 second,
1081 path_to_buffer,
1082 terminal_buffers,
1083 unnamed_buffers,
1084 split_states,
1085 split_id_map,
1086 false,
1087 );
1088 }
1089 Err(e) => {
1090 tracing::error!("Failed to create split during workspace restore: {}", e);
1091 }
1092 }
1093 }
1094 }
1095 }
1096
1097 fn restore_split_view_state(
1099 &mut self,
1100 current_split_id: LeafId,
1101 saved_split_id: usize,
1102 split_states: &HashMap<usize, SerializedSplitViewState>,
1103 path_to_buffer: &HashMap<PathBuf, BufferId>,
1104 terminal_buffers: &HashMap<usize, BufferId>,
1105 unnamed_buffers: &HashMap<String, BufferId>,
1106 ) {
1107 let Some(split_state) = split_states.get(&saved_split_id) else {
1109 return;
1110 };
1111
1112 let split_buf_for_current = self
1116 .buffers
1117 .split_manager()
1118 .expect("active window must have a populated split layout")
1119 .buffer_for_split(current_split_id);
1120 let active_buffer_id = self
1121 .buffers
1122 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1123 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1124 return None;
1125 };
1126 let mut active_buffer_id: Option<BufferId> = None;
1127 if !split_state.open_tabs.is_empty() {
1128 view_state.open_buffers.clear();
1131
1132 for tab in &split_state.open_tabs {
1133 match tab {
1134 SerializedTabRef::File(rel_path) => {
1135 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1136 if !view_state.has_buffer(buffer_id) {
1137 view_state.add_buffer(buffer_id);
1138 }
1139 view_state.ensure_buffer_state(buffer_id);
1141 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1142 let buf_state =
1143 view_state.buffer_state_mut(buffer_id).unwrap();
1144 buf_state.viewport.line_wrap_enabled = false;
1145 buf_state.show_line_numbers = false;
1149 buf_state.highlight_current_line = false;
1150 }
1151 }
1152 }
1153 SerializedTabRef::Terminal(index) => {
1154 if let Some(&buffer_id) = terminal_buffers.get(index) {
1155 if !view_state.has_buffer(buffer_id) {
1156 view_state.add_buffer(buffer_id);
1157 }
1158 let buf_state = view_state.ensure_buffer_state(buffer_id);
1159 buf_state.viewport.line_wrap_enabled = false;
1160 buf_state.show_line_numbers = false;
1164 buf_state.highlight_current_line = false;
1165 }
1166 }
1167 SerializedTabRef::Unnamed(recovery_id) => {
1168 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1169 if !view_state.has_buffer(buffer_id) {
1170 view_state.add_buffer(buffer_id);
1171 }
1172 view_state.ensure_buffer_state(buffer_id);
1173 }
1174 }
1175 }
1176 }
1177
1178 if view_state.open_buffers.is_empty() {
1183 if let Some(buf) = split_buf_for_current {
1184 view_state.add_buffer(buf);
1185 view_state.ensure_buffer_state(buf);
1186 }
1187 }
1188
1189 if let Some(active_idx) = split_state.active_tab_index {
1190 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1191 active_buffer_id = match tab {
1192 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1193 SerializedTabRef::Terminal(index) => {
1194 terminal_buffers.get(index).copied()
1195 }
1196 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1197 };
1198 }
1199 }
1200 } else {
1201 for rel_path in &split_state.open_files {
1203 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1204 if !view_state.has_buffer(buffer_id) {
1205 view_state.add_buffer(buffer_id);
1206 }
1207 view_state.ensure_buffer_state(buffer_id);
1208 }
1209 }
1210
1211 let active_file_path =
1212 split_state.open_files.get(split_state.active_file_index);
1213 active_buffer_id =
1214 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1215 }
1216
1217 for (rel_path, file_state) in &split_state.file_states {
1219 let rel_str = rel_path.to_string_lossy();
1221 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1222 match unnamed_buffers.get(recovery_id).copied() {
1223 Some(id) => id,
1224 None => continue,
1225 }
1226 } else {
1227 match path_to_buffer.get(rel_path).copied() {
1228 Some(id) => id,
1229 None => continue,
1230 }
1231 };
1232 let max_pos = __buffers_mut
1233 .get(&buffer_id)
1234 .map(|b| b.buffer.len())
1235 .unwrap_or(0);
1236
1237 let buf_state = view_state.ensure_buffer_state(buffer_id);
1239
1240 let cursor_pos = file_state.cursor.position.min(max_pos);
1241 buf_state.cursors.primary_mut().position = cursor_pos;
1242 buf_state.cursors.primary_mut().anchor =
1243 file_state.cursor.anchor.map(|a| a.min(max_pos));
1244 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1245
1246 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1247 buf_state.viewport.top_view_line_offset =
1248 file_state.scroll.top_view_line_offset;
1249 buf_state.viewport.left_column = file_state.scroll.left_column;
1250 buf_state.viewport.set_skip_resize_sync();
1251
1252 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1260 super::navigation::reconcile_restored_buffer_view(
1261 buf_state,
1262 &mut state.buffer,
1263 );
1264 }
1265
1266 buf_state.view_mode = match file_state.view_mode {
1268 SerializedViewMode::Source => ViewMode::Source,
1269 SerializedViewMode::PageView => ViewMode::PageView,
1270 };
1271 buf_state.compose_width = file_state.compose_width;
1272 buf_state.plugin_state = file_state.plugin_state.clone();
1273 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1274 buf_state.folds.clear(&mut state.marker_list);
1275 for fold in &file_state.folds {
1276 let Some(resolved_header) = resolve_fold_header_line(
1283 &state.buffer,
1284 fold.header_line,
1285 fold.header_text.as_deref(),
1286 ) else {
1287 tracing::debug!(
1288 "Dropping stale fold: header_line={} no longer matches stored \
1289 header_text after external edit",
1290 fold.header_line,
1291 );
1292 continue;
1293 };
1294
1295 let shift = resolved_header as i64 - fold.header_line as i64;
1297 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1298 let start_line = resolved_header.saturating_add(1);
1299 let end_line = adjusted_end;
1300 if start_line > end_line {
1301 continue;
1302 }
1303 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1304 else {
1305 continue;
1306 };
1307 let end_byte = state
1308 .buffer
1309 .line_start_offset(end_line.saturating_add(1))
1310 .unwrap_or_else(|| state.buffer.len());
1311 buf_state.folds.add(
1312 &mut state.marker_list,
1313 start_byte,
1314 end_byte,
1315 fold.placeholder.clone(),
1316 );
1317 }
1318 }
1319
1320 tracing::trace!(
1321 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1322 rel_path,
1323 cursor_pos,
1324 buf_state.viewport.top_byte,
1325 buf_state.view_mode,
1326 );
1327 }
1328
1329 if active_buffer_id.is_none() {
1346 active_buffer_id = view_state.buffer_tab_ids().next();
1347 }
1348
1349 let restored_view_mode = match split_state.view_mode {
1352 SerializedViewMode::Source => ViewMode::Source,
1353 SerializedViewMode::PageView => ViewMode::PageView,
1354 };
1355
1356 if let Some(active_buf_id) = active_buffer_id {
1357 view_state.switch_buffer(active_buf_id);
1359
1360 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
1362 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
1363 });
1364 if !active_has_file_state {
1365 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1366 view_state.active_state_mut().compose_width = split_state.compose_width;
1367 }
1368
1369 }
1371 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1372 active_buffer_id
1373 })
1374 .flatten();
1375
1376 if let Some(active_buf_id) = active_buffer_id {
1380 self.buffers
1381 .split_manager_mut()
1382 .expect("active window must have a populated split layout")
1383 .set_split_buffer(current_split_id, active_buf_id);
1384 }
1385 }
1386
1387 fn restore_search_options(&mut self, opts: &SearchOptions) {
1388 self.search_case_sensitive = opts.case_sensitive;
1389 self.search_whole_word = opts.whole_word;
1390 self.search_use_regex = opts.use_regex;
1391 self.search_confirm_each = opts.confirm_each;
1392 }
1393
1394 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
1395 tracing::debug!(
1396 "Restoring histories: {} search, {} replace, {} goto_line",
1397 histories.search.len(),
1398 histories.replace.len(),
1399 histories.goto_line.len()
1400 );
1401 for item in &histories.search {
1402 self.prompt_histories
1403 .entry("search".to_string())
1404 .or_default()
1405 .push(item.clone());
1406 }
1407 for item in &histories.replace {
1408 self.prompt_histories
1409 .entry("replace".to_string())
1410 .or_default()
1411 .push(item.clone());
1412 }
1413 for item in &histories.goto_line {
1414 self.prompt_histories
1415 .entry("goto_line".to_string())
1416 .or_default()
1417 .push(item.clone());
1418 }
1419 }
1420
1421 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
1422 self.file_explorer_visible = fe.visible;
1423 self.file_explorer_width = fe.width;
1424 self.file_explorer_side = fe.side;
1425
1426 if fe.show_hidden {
1428 self.pending_file_explorer_show_hidden = Some(true);
1429 }
1430 if fe.show_gitignored {
1431 self.pending_file_explorer_show_gitignored = Some(true);
1432 }
1433
1434 if self.file_explorer_visible && self.file_explorer.is_none() {
1436 self.init_file_explorer();
1437 }
1438 }
1439
1440 fn open_workspace_files(
1443 &mut self,
1444 split_states: &HashMap<usize, SerializedSplitViewState>,
1445 ) -> HashMap<PathBuf, BufferId> {
1446 let file_paths = collect_file_paths_from_states(split_states);
1447 tracing::debug!(
1448 "Workspace has {} files to restore: {:?}",
1449 file_paths.len(),
1450 file_paths
1451 );
1452 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
1453 for rel_path in file_paths {
1454 let abs_path = self.root.join(&rel_path);
1455 tracing::trace!(
1456 "Checking file: {:?} (exists: {})",
1457 abs_path,
1458 abs_path.exists()
1459 );
1460 if abs_path.exists() {
1461 match self.open_file_internal(&abs_path) {
1462 Ok(buffer_id) => {
1463 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
1464 path_to_buffer.insert(rel_path, buffer_id);
1465 }
1466 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
1467 }
1468 } else {
1469 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
1470 }
1471 }
1472 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
1473 path_to_buffer
1474 }
1475
1476 fn restore_external_files(
1478 &mut self,
1479 external_files: &[PathBuf],
1480 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
1481 ) {
1482 if external_files.is_empty() {
1483 return;
1484 }
1485 tracing::debug!(
1486 "Restoring {} external files: {:?}",
1487 external_files.len(),
1488 external_files
1489 );
1490 for abs_path in external_files {
1491 if !abs_path.exists() {
1492 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
1493 continue;
1494 }
1495 match self.open_file_internal(abs_path) {
1496 Ok(buffer_id) => {
1497 path_to_buffer.insert(abs_path.clone(), buffer_id);
1498 tracing::debug!(
1499 "Restored external file {:?} as buffer {:?}",
1500 abs_path,
1501 buffer_id
1502 );
1503 }
1504 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1505 }
1506 }
1507 }
1508
1509 fn apply_read_only_flags(
1512 &mut self,
1513 read_only_files: &[PathBuf],
1514 path_to_buffer: &HashMap<PathBuf, BufferId>,
1515 ) {
1516 for ro_path in read_only_files {
1517 let buffer_id = path_to_buffer
1518 .get(ro_path)
1519 .copied()
1520 .or_else(|| path_to_buffer.get(&self.root.join(ro_path)).copied());
1521 if let Some(id) = buffer_id {
1522 self.mark_buffer_read_only(id, true);
1523 }
1524 }
1525 }
1526
1527 pub(crate) fn has_any_virtual_buffer(&self) -> bool {
1532 self.buffer_metadata
1533 .values()
1534 .any(|m| matches!(m.kind, crate::app::types::BufferKind::Virtual { .. }))
1535 }
1536
1537 pub(crate) fn save_all_global_file_states(&self) {
1540 for (leaf_id, view_state) in self
1541 .buffers
1542 .splits()
1543 .map(|(_, vs)| vs)
1544 .expect("window must have a populated split layout")
1545 {
1546 let active_buffer = self
1547 .buffers
1548 .splits()
1549 .map(|(mgr, _)| mgr)
1550 .expect("window must have a populated split layout")
1551 .root()
1552 .get_leaves_with_rects(ratatui::layout::Rect::default())
1553 .into_iter()
1554 .find(|(sid, _, _)| *sid == *leaf_id)
1555 .map(|(_, buffer_id, _)| buffer_id);
1556
1557 if let Some(buffer_id) = active_buffer {
1558 self.save_buffer_file_state(buffer_id, view_state);
1559 }
1560 }
1561 }
1562
1563 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
1565 let abs_path = match self.buffer_metadata.get(&buffer_id) {
1566 Some(metadata) => match metadata.file_path() {
1567 Some(path) => path.to_path_buf(),
1568 None => return,
1569 },
1570 None => return,
1571 };
1572
1573 let primary_cursor = view_state.cursors.primary();
1574 let file_state = SerializedFileState {
1575 cursor: SerializedCursor {
1576 position: primary_cursor.position,
1577 anchor: primary_cursor.anchor,
1578 sticky_column: primary_cursor.sticky_column,
1579 },
1580 additional_cursors: view_state
1581 .cursors
1582 .iter()
1583 .skip(1)
1584 .map(|(_, cursor)| SerializedCursor {
1585 position: cursor.position,
1586 anchor: cursor.anchor,
1587 sticky_column: cursor.sticky_column,
1588 })
1589 .collect(),
1590 scroll: SerializedScroll {
1591 top_byte: view_state.viewport.top_byte,
1592 top_view_line_offset: view_state.viewport.top_view_line_offset,
1593 left_column: view_state.viewport.left_column,
1594 },
1595 view_mode: Default::default(),
1596 compose_width: None,
1597 plugin_state: std::collections::HashMap::new(),
1598 folds: Vec::new(),
1599 };
1600
1601 PersistedFileWorkspace::save(&abs_path, file_state);
1602 }
1603
1604 pub(crate) fn sync_terminal_backing_files(&self) {
1607 use std::io::BufWriter;
1608
1609 let terminals_to_sync: Vec<_> = self
1610 .terminal_buffers
1611 .values()
1612 .copied()
1613 .filter_map(|terminal_id| {
1614 self.terminal_backing_files
1615 .get(&terminal_id)
1616 .map(|path| (terminal_id, path.clone()))
1617 })
1618 .collect();
1619
1620 for (terminal_id, backing_path) in terminals_to_sync {
1621 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1622 if let Ok(mut state) = handle.state.lock() {
1623 if let Ok(mut file) = self
1628 .authority()
1629 .filesystem
1630 .open_file_for_append(&backing_path)
1631 {
1632 let mut writer = BufWriter::new(&mut *file);
1633 if let Err(e) = state.flush_new_scrollback(&mut writer) {
1634 tracing::warn!(
1635 "Failed to flush terminal {:?} scrollback: {}",
1636 terminal_id,
1637 e
1638 );
1639 }
1640 }
1641
1642 if let Ok(mut file) = self
1643 .authority()
1644 .filesystem
1645 .open_file_for_append(&backing_path)
1646 {
1647 let mut writer = BufWriter::new(&mut *file);
1648 if let Err(e) = state.append_visible_screen(&mut writer) {
1649 tracing::warn!(
1650 "Failed to sync terminal {:?} to backing file: {}",
1651 terminal_id,
1652 e
1653 );
1654 }
1655 }
1656 }
1657 }
1658 }
1659 }
1660
1661 pub(crate) fn create_unnamed_recovery_buffer(
1665 &mut self,
1666 text: &str,
1667 recovery_id: String,
1668 display_name: String,
1669 ) -> BufferId {
1670 let buffer_id = self.alloc_buffer_id();
1671 let mut state = EditorState::new(
1672 self.terminal_width,
1673 self.terminal_height,
1674 self.resources.config.editor.large_file_threshold_bytes as usize,
1675 std::sync::Arc::clone(&self.authority().filesystem),
1676 );
1677 state
1678 .margins
1679 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1680 state.buffer.set_default_line_ending(
1681 self.resources
1682 .config
1683 .editor
1684 .default_line_ending
1685 .to_line_ending(),
1686 );
1687 state.buffer.insert(0, text);
1688 state.buffer.set_modified(true);
1689 state.buffer.set_recovery_pending(false);
1690 self.buffers.insert(buffer_id, state);
1691
1692 let mut log = crate::model::event::EventLog::new();
1693 log.clear_saved_position();
1694 self.event_logs.insert(buffer_id, log);
1695
1696 let mut meta = crate::app::types::BufferMetadata::new();
1697 meta.recovery_id = Some(recovery_id);
1698 meta.display_name = display_name;
1699 self.buffer_metadata.insert(buffer_id, meta);
1700
1701 buffer_id
1702 }
1703
1704 pub(crate) fn seed_initial_layout(&mut self) {
1708 if self.buffers.splits().is_some() && self.buffers.len() > 0 {
1709 return;
1710 }
1711 let buf = self.alloc_buffer_id();
1712 let mut state = EditorState::new(
1713 self.terminal_width,
1714 self.terminal_height,
1715 self.resources.config.editor.large_file_threshold_bytes as usize,
1716 std::sync::Arc::clone(&self.authority().filesystem),
1717 );
1718 state
1719 .margins
1720 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1721 state.buffer.set_default_line_ending(
1722 self.resources
1723 .config
1724 .editor
1725 .default_line_ending
1726 .to_line_ending(),
1727 );
1728 let manager = crate::view::split::SplitManager::new(buf);
1729 let active_leaf = manager.active_split();
1730 let mut view_states = HashMap::new();
1731 view_states.insert(
1732 active_leaf,
1733 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buf),
1734 );
1735 self.buffers.set_splits((manager, view_states));
1736 self.buffers.insert(buf, state);
1737 self.buffer_metadata
1738 .insert(buf, crate::app::types::BufferMetadata::new());
1739 self.event_logs
1740 .insert(buf, crate::model::event::EventLog::new());
1741 }
1742
1743 pub(crate) fn sync_lsp_after_recovery_replay(&mut self, buffer_id: BufferId) {
1747 let Some(text) = self
1748 .buffers
1749 .get(&buffer_id)
1750 .and_then(|state| state.buffer.to_string())
1751 else {
1752 return;
1753 };
1754 let full_change = lsp_types::TextDocumentContentChangeEvent {
1755 range: None,
1756 range_length: None,
1757 text,
1758 };
1759 self.send_lsp_changes_for_buffer(buffer_id, vec![full_change]);
1760 }
1761
1762 fn restore_unnamed_buffers(
1768 &mut self,
1769 unnamed_buffers: &[UnnamedBufferRef],
1770 ) -> HashMap<String, BufferId> {
1771 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1772 if !self.resources.config.editor.hot_exit || unnamed_buffers.is_empty() {
1773 return unnamed_buffer_map;
1774 }
1775 tracing::debug!(
1776 "Restoring {} unnamed buffers from recovery",
1777 unnamed_buffers.len()
1778 );
1779 for unnamed_ref in unnamed_buffers {
1780 let entries = match self
1781 .resources
1782 .recovery_service
1783 .lock()
1784 .unwrap()
1785 .list_recoverable()
1786 {
1787 Ok(e) => e,
1788 Err(e) => {
1789 tracing::warn!("Failed to list recovery entries: {}", e);
1790 continue;
1791 }
1792 };
1793 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1794 tracing::debug!(
1795 "Recovery file not found for unnamed buffer {}",
1796 unnamed_ref.recovery_id
1797 );
1798 continue;
1799 };
1800 let loaded = self
1801 .resources
1802 .recovery_service
1803 .lock()
1804 .unwrap()
1805 .load_recovery(entry);
1806 match loaded {
1807 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1808 let text = String::from_utf8_lossy(&content).into_owned();
1809 let buffer_id = self.create_unnamed_recovery_buffer(
1810 &text,
1811 unnamed_ref.recovery_id.clone(),
1812 unnamed_ref.display_name.clone(),
1813 );
1814 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1815 tracing::info!(
1816 "Restored unnamed buffer '{}' (recovery_id={})",
1817 unnamed_ref.display_name,
1818 unnamed_ref.recovery_id
1819 );
1820 }
1821 Ok(other) => {
1822 tracing::warn!(
1823 "Unexpected recovery result for unnamed buffer {}: {:?}",
1824 unnamed_ref.recovery_id,
1825 std::mem::discriminant(&other)
1826 );
1827 }
1828 Err(e) => {
1829 tracing::warn!(
1830 "Failed to load recovery for unnamed buffer {}: {}",
1831 unnamed_ref.recovery_id,
1832 e
1833 );
1834 }
1835 }
1836 }
1837 unnamed_buffer_map
1838 }
1839
1840 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1844 if !self.resources.config.editor.hot_exit {
1845 return;
1846 }
1847 let entries = self
1848 .resources
1849 .recovery_service
1850 .lock()
1851 .unwrap()
1852 .list_recoverable()
1853 .unwrap_or_default();
1854 if entries.is_empty() {
1855 return;
1856 }
1857 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1858 for buffer_id in buffer_ids {
1859 let file_path = self
1860 .buffers
1861 .get(&buffer_id)
1862 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1863 let Some(file_path) = file_path else { continue };
1864
1865 let recovery_id = self
1866 .resources
1867 .recovery_service
1868 .lock()
1869 .unwrap()
1870 .get_buffer_id(Some(&file_path));
1871 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1872 continue;
1873 };
1874 let loaded = self
1875 .resources
1876 .recovery_service
1877 .lock()
1878 .unwrap()
1879 .load_recovery(entry);
1880 match loaded {
1881 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1882 let mut mutated = false;
1883 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1884 let current_len = state.buffer.total_bytes();
1885 let text = String::from_utf8_lossy(&content).into_owned();
1886 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1887 let current_text = current
1888 .as_ref()
1889 .map(|b| String::from_utf8_lossy(b).into_owned());
1890 if current_text.as_deref() != Some(&text) {
1891 state.buffer.delete(0..current_len);
1892 state.buffer.insert(0, &text);
1893 state.buffer.set_modified(true);
1894 state.buffer.set_recovery_pending(false);
1895 mutated = true;
1896 tracing::info!(
1897 "Restored unsaved changes for {:?} from hot exit recovery",
1898 file_path
1899 );
1900 }
1901 }
1902 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1903 log.clear_saved_position();
1904 }
1905 if mutated {
1906 self.sync_lsp_after_recovery_replay(buffer_id);
1907 }
1908 }
1909 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
1910 chunks, ..
1911 }) => {
1912 let mut mutated = false;
1913 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1914 for chunk in chunks.into_iter().rev() {
1915 let text = String::from_utf8_lossy(&chunk.content).into_owned();
1916 if chunk.original_len > 0 {
1917 state
1918 .buffer
1919 .delete(chunk.offset..chunk.offset + chunk.original_len);
1920 }
1921 state.buffer.insert(chunk.offset, &text);
1922 }
1923 state.buffer.set_modified(true);
1924 state.buffer.set_recovery_pending(false);
1925 mutated = true;
1926 tracing::info!(
1927 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
1928 file_path
1929 );
1930 }
1931 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1932 log.clear_saved_position();
1933 }
1934 if mutated {
1935 self.sync_lsp_after_recovery_replay(buffer_id);
1936 }
1937 }
1938 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1939 original_path,
1940 ..
1941 }) => {
1942 let name = original_path
1943 .file_name()
1944 .unwrap_or_default()
1945 .to_string_lossy();
1946 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1947 self.set_status_message(format!(
1948 "{} changed on disk; unsaved changes not restored",
1949 name
1950 ));
1951 }
1952 Ok(_) => {} Err(e) => {
1954 tracing::debug!(
1955 "Failed to load hot exit recovery for {:?}: {}",
1956 file_path,
1957 e
1958 );
1959 }
1960 }
1961 }
1962 }
1963
1964 pub(crate) fn apply_workspace_layout(
1980 &mut self,
1981 workspace: &Workspace,
1982 session_name: Option<&str>,
1983 ) {
1984 tracing::debug!(
1985 "Applying workspace layout with {} split states",
1986 workspace.split_states.len()
1987 );
1988
1989 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
1992 self.mouse_enabled = mouse_enabled;
1993 }
1994
1995 self.restore_search_options(&workspace.search_options);
1996 self.restore_prompt_histories(&workspace.histories);
1997 self.restore_file_explorer_settings(&workspace.file_explorer);
1998
1999 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
2002
2003 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
2004 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
2005 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
2006
2007 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
2008
2009 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
2010 self.restore_split_node(
2011 &workspace.split_layout,
2012 &path_to_buffer,
2013 &terminal_buffer_map,
2014 &unnamed_buffer_map,
2015 &workspace.split_states,
2016 &mut split_id_map,
2017 true,
2018 );
2019
2020 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
2021 self.buffers
2022 .split_manager_mut()
2023 .expect("window must have a populated split layout")
2024 .set_active_split(LeafId(new_active_split));
2025 }
2026
2027 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
2028 self.clean_orphaned_buffers();
2029 self.log_restore_summary(session_name);
2030
2031 self.restore_hot_exit_changes(&path_to_buffer);
2033 }
2034
2035 pub(crate) fn from_workspace(
2041 id: fresh_core::WindowId,
2042 label: impl Into<String>,
2043 root: PathBuf,
2044 authority: crate::services::authority::Authority,
2045 resources: crate::app::window_resources::WindowResources,
2046 workspace: &Workspace,
2047 ) -> Self {
2048 let mut window = Self::new(id, label, root, authority, resources);
2049 window.seed_initial_layout();
2050 window.apply_workspace_layout(workspace, None);
2051 window
2052 }
2053
2054 pub(crate) fn capture_workspace(&self) -> Workspace {
2060 tracing::debug!("Capturing workspace for {:?}", self.root);
2061
2062 let mut terminals = Vec::new();
2063 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
2064 let mut seen = HashSet::new();
2065 for terminal_id in self.terminal_buffers.values().copied() {
2066 if seen.insert(terminal_id) {
2067 let command = self.terminal_commands.get(&terminal_id).cloned();
2068 if self.ephemeral_terminals.contains(&terminal_id) && command.is_none() {
2076 continue;
2077 }
2078 let idx = terminals.len();
2079 terminal_indices.insert(terminal_id, idx);
2080 let handle = self.terminal_manager.get(terminal_id);
2081 let (cols, rows) = handle
2082 .map(|h| h.size())
2083 .unwrap_or((self.terminal_width, self.terminal_height));
2084 let cwd = handle.and_then(|h| h.cwd());
2085 let shell = handle
2086 .map(|h| h.shell().to_string())
2087 .unwrap_or_else(crate::services::terminal::detect_shell);
2088 let log_path = self
2089 .terminal_log_files
2090 .get(&terminal_id)
2091 .cloned()
2092 .unwrap_or_else(|| {
2093 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2094 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
2095 });
2096 let backing_path = self
2097 .terminal_backing_files
2098 .get(&terminal_id)
2099 .cloned()
2100 .unwrap_or_else(|| {
2101 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2102 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
2103 });
2104
2105 let agent_resume = self
2106 .terminal_resume_commands
2107 .get(&terminal_id)
2108 .filter(|argv| !argv.is_empty())
2109 .map(|argv| crate::workspace::AgentResume { argv: argv.clone() });
2110 terminals.push(SerializedTerminalWorkspace {
2111 terminal_index: idx,
2112 cwd,
2113 shell,
2114 cols,
2115 rows,
2116 log_path,
2117 backing_path,
2118 command,
2119 agent_resume,
2120 });
2121 }
2122 }
2123
2124 let (mgr, view_states) = self
2125 .buffers
2126 .splits()
2127 .expect("window must have a populated split layout");
2128
2129 let split_layout = serialize_split_node(
2130 mgr.root(),
2131 &self.buffer_metadata,
2132 &self.root,
2133 &self.terminal_buffers,
2134 &terminal_indices,
2135 mgr.labels(),
2136 );
2137
2138 let active_buffers: HashMap<LeafId, BufferId> = mgr
2139 .root()
2140 .get_leaves_with_rects(ratatui::layout::Rect::default())
2141 .into_iter()
2142 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
2143 .collect();
2144
2145 let mut split_states = HashMap::new();
2146 for (leaf_id, view_state) in view_states {
2147 let active_buffer = active_buffers.get(leaf_id).copied();
2148 let serialized = serialize_split_view_state(
2149 view_state,
2150 self.buffers.as_map(),
2151 &self.buffer_metadata,
2152 &self.root,
2153 active_buffer,
2154 &self.terminal_buffers,
2155 &terminal_indices,
2156 );
2157 split_states.insert(leaf_id.0 .0, serialized);
2158 }
2159
2160 let file_explorer = if let Some(explorer) = self.file_explorer.as_ref() {
2161 let expanded_dirs = get_expanded_dirs(explorer, &self.root);
2162 FileExplorerState {
2163 visible: self.file_explorer_visible,
2164 width: self.file_explorer_width,
2165 side: self.file_explorer_side,
2166 expanded_dirs,
2167 scroll_offset: explorer.get_scroll_offset(),
2168 show_hidden: explorer.ignore_patterns().show_hidden(),
2169 show_gitignored: explorer.ignore_patterns().show_gitignored(),
2170 }
2171 } else {
2172 FileExplorerState {
2173 visible: self.file_explorer_visible,
2174 width: self.file_explorer_width,
2175 side: self.file_explorer_side,
2176 expanded_dirs: Vec::new(),
2177 scroll_offset: 0,
2178 show_hidden: false,
2179 show_gitignored: false,
2180 }
2181 };
2182
2183 let cfg = &self.resources.config.editor;
2184 let config_overrides = WorkspaceConfigOverrides {
2185 line_numbers: Some(cfg.line_numbers),
2186 relative_line_numbers: Some(cfg.relative_line_numbers),
2187 line_wrap: Some(cfg.line_wrap),
2188 syntax_highlighting: Some(cfg.syntax_highlighting),
2189 enable_inlay_hints: Some(cfg.enable_inlay_hints),
2190 mouse_enabled: Some(self.mouse_enabled),
2191 menu_bar_hidden: None,
2192 };
2193
2194 let histories = WorkspaceHistories {
2195 search: self
2196 .prompt_histories
2197 .get("search")
2198 .map(|h| h.items().to_vec())
2199 .unwrap_or_default(),
2200 replace: self
2201 .prompt_histories
2202 .get("replace")
2203 .map(|h| h.items().to_vec())
2204 .unwrap_or_default(),
2205 command_palette: Vec::new(),
2206 goto_line: self
2207 .prompt_histories
2208 .get("goto_line")
2209 .map(|h| h.items().to_vec())
2210 .unwrap_or_default(),
2211 open_file: Vec::new(),
2212 };
2213
2214 let search_options = SearchOptions {
2215 case_sensitive: self.search_case_sensitive,
2216 whole_word: self.search_whole_word,
2217 use_regex: self.search_use_regex,
2218 confirm_each: self.search_confirm_each,
2219 };
2220
2221 let bookmarks = serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.root);
2222
2223 let external_files: Vec<PathBuf> = self
2224 .buffer_metadata
2225 .values()
2226 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2227 .filter_map(|meta| meta.file_path())
2228 .filter(|abs_path| abs_path.strip_prefix(&self.root).is_err())
2229 .cloned()
2230 .collect();
2231
2232 let read_only_files: Vec<PathBuf> = self
2233 .buffer_metadata
2234 .values()
2235 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2236 .filter(|meta| meta.read_only)
2237 .filter_map(|meta| meta.file_path().cloned())
2238 .filter(|p| !p.as_os_str().is_empty())
2239 .map(|p| {
2240 p.strip_prefix(&self.root)
2241 .map(|rel| rel.to_path_buf())
2242 .unwrap_or(p)
2243 })
2244 .collect();
2245
2246 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.resources.config.editor.hot_exit {
2247 self.buffer_metadata
2248 .iter()
2249 .filter_map(|(buffer_id, meta)| {
2250 let path = meta.file_path()?;
2251 if !path.as_os_str().is_empty() {
2252 return None;
2253 }
2254 if meta.hidden_from_tabs || meta.is_virtual() {
2255 return None;
2256 }
2257 let state = self.buffers.get(buffer_id)?;
2258 if state.buffer.total_bytes() == 0 {
2259 return None;
2260 }
2261 let recovery_id = meta.recovery_id.clone()?;
2262 Some(UnnamedBufferRef {
2263 recovery_id,
2264 display_name: meta.display_name.clone(),
2265 })
2266 })
2267 .collect()
2268 } else {
2269 Vec::new()
2270 };
2271
2272 Workspace {
2273 version: WORKSPACE_VERSION,
2274 working_dir: self.root.clone(),
2275 split_layout,
2276 active_split_id: SplitId::from(mgr.active_split()).0,
2277 split_states,
2278 config_overrides,
2279 file_explorer,
2280 histories,
2281 search_options,
2282 bookmarks,
2283 terminals,
2284 external_files,
2285 read_only_files,
2286 unnamed_buffers,
2287 plugin_global_state: HashMap::new(),
2288 saved_at: std::time::SystemTime::now()
2289 .duration_since(std::time::UNIX_EPOCH)
2290 .unwrap_or_default()
2291 .as_secs(),
2292 label: Some(self.label.clone()),
2295 session_plugin_state: self.plugin_state.clone(),
2296 authority_spec: self.authority_spec.clone(),
2298 }
2299 }
2300}
2301
2302fn get_first_leaf_buffer(
2304 node: &SerializedSplitNode,
2305 path_to_buffer: &HashMap<PathBuf, BufferId>,
2306 terminal_buffers: &HashMap<usize, BufferId>,
2307 unnamed_buffers: &HashMap<String, BufferId>,
2308) -> Option<BufferId> {
2309 match node {
2310 SerializedSplitNode::Leaf {
2311 file_path,
2312 unnamed_recovery_id,
2313 ..
2314 } => file_path
2315 .as_ref()
2316 .and_then(|p| path_to_buffer.get(p).copied())
2317 .or_else(|| {
2318 unnamed_recovery_id
2319 .as_ref()
2320 .and_then(|id| unnamed_buffers.get(id).copied())
2321 }),
2322 SerializedSplitNode::Terminal { terminal_index, .. } => {
2323 terminal_buffers.get(terminal_index).copied()
2324 }
2325 SerializedSplitNode::Split { first, .. } => {
2326 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2327 }
2328 }
2329}
2330
2331fn serialize_split_node(
2336 node: &SplitNode,
2337 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2338 working_dir: &Path,
2339 terminal_buffers: &HashMap<BufferId, TerminalId>,
2340 terminal_indices: &HashMap<TerminalId, usize>,
2341 split_labels: &HashMap<SplitId, String>,
2342) -> SerializedSplitNode {
2343 serialize_split_node_pruned(
2344 node,
2345 buffer_metadata,
2346 working_dir,
2347 terminal_buffers,
2348 terminal_indices,
2349 split_labels,
2350 )
2351 .unwrap_or({
2352 SerializedSplitNode::Leaf {
2355 file_path: None,
2356 split_id: 0,
2357 label: None,
2358 unnamed_recovery_id: None,
2359 role: None,
2360 }
2361 })
2362}
2363
2364fn serialize_split_node_pruned(
2371 node: &SplitNode,
2372 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2373 working_dir: &Path,
2374 terminal_buffers: &HashMap<BufferId, TerminalId>,
2375 terminal_indices: &HashMap<TerminalId, usize>,
2376 split_labels: &HashMap<SplitId, String>,
2377) -> Option<SerializedSplitNode> {
2378 match node {
2379 SplitNode::Grouped { layout, .. } => {
2380 serialize_split_node_pruned(
2384 layout,
2385 buffer_metadata,
2386 working_dir,
2387 terminal_buffers,
2388 terminal_indices,
2389 split_labels,
2390 )
2391 }
2392 SplitNode::Leaf {
2393 buffer_id,
2394 split_id,
2395 role,
2396 } => {
2397 let raw_split_id: SplitId = (*split_id).into();
2398 let label = split_labels.get(&raw_split_id).cloned();
2399 let role = *role;
2400
2401 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2402 if let Some(index) = terminal_indices.get(terminal_id) {
2403 return Some(SerializedSplitNode::Terminal {
2404 terminal_index: *index,
2405 split_id: raw_split_id.0,
2406 label,
2407 role,
2408 });
2409 }
2410 }
2411
2412 let meta = buffer_metadata.get(buffer_id);
2413
2414 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2418 return None;
2419 }
2420
2421 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2422 if abs_path.as_os_str().is_empty() {
2423 None } else {
2425 abs_path
2426 .strip_prefix(working_dir)
2427 .ok()
2428 .map(|p| p.to_path_buf())
2429 }
2430 });
2431
2432 let unnamed_recovery_id = if file_path.is_none() {
2435 meta.and_then(|m| m.recovery_id.clone())
2436 } else {
2437 None
2438 };
2439
2440 Some(SerializedSplitNode::Leaf {
2441 file_path,
2442 split_id: raw_split_id.0,
2443 label,
2444 unnamed_recovery_id,
2445 role,
2446 })
2447 }
2448 SplitNode::Split {
2449 direction,
2450 first,
2451 second,
2452 ratio,
2453 split_id,
2454 ..
2455 } => {
2456 let raw_split_id: SplitId = (*split_id).into();
2457 let first = serialize_split_node_pruned(
2458 first,
2459 buffer_metadata,
2460 working_dir,
2461 terminal_buffers,
2462 terminal_indices,
2463 split_labels,
2464 );
2465 let second = serialize_split_node_pruned(
2466 second,
2467 buffer_metadata,
2468 working_dir,
2469 terminal_buffers,
2470 terminal_indices,
2471 split_labels,
2472 );
2473 match (first, second) {
2474 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2475 direction: match direction {
2476 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2477 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2478 },
2479 first: Box::new(f),
2480 second: Box::new(s),
2481 ratio: *ratio,
2482 split_id: raw_split_id.0,
2483 }),
2484 (Some(only), None) | (None, Some(only)) => Some(only),
2487 (None, None) => None,
2488 }
2489 }
2490 }
2491}
2492
2493fn serialize_split_view_state(
2494 view_state: &crate::view::split::SplitViewState,
2495 buffers: &HashMap<BufferId, EditorState>,
2496 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2497 working_dir: &Path,
2498 active_buffer: Option<BufferId>,
2499 terminal_buffers: &HashMap<BufferId, TerminalId>,
2500 terminal_indices: &HashMap<TerminalId, usize>,
2501) -> SerializedSplitViewState {
2502 let mut open_tabs = Vec::new();
2503 let mut open_files = Vec::new();
2504 let mut active_tab_index = None;
2505
2506 for buffer_id in view_state.buffer_tab_ids() {
2508 let buffer_id = &buffer_id;
2509 let tab_index = open_tabs.len();
2510 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2511 if let Some(idx) = terminal_indices.get(terminal_id) {
2512 open_tabs.push(SerializedTabRef::Terminal(*idx));
2513 if Some(*buffer_id) == active_buffer {
2514 active_tab_index = Some(tab_index);
2515 }
2516 continue;
2517 }
2518 }
2519
2520 if let Some(meta) = buffer_metadata.get(buffer_id) {
2521 if let Some(abs_path) = meta.file_path() {
2522 if abs_path.as_os_str().is_empty() {
2523 if let Some(ref recovery_id) = meta.recovery_id {
2525 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2526 if Some(*buffer_id) == active_buffer {
2527 active_tab_index = Some(tab_index);
2528 }
2529 }
2530 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2531 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2532 open_files.push(rel_path.to_path_buf());
2533 if Some(*buffer_id) == active_buffer {
2534 active_tab_index = Some(tab_index);
2535 }
2536 } else {
2537 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2539 if Some(*buffer_id) == active_buffer {
2540 active_tab_index = Some(tab_index);
2541 }
2542 }
2543 }
2544 }
2545 }
2546
2547 let active_file_index = active_tab_index
2549 .and_then(|idx| open_tabs.get(idx))
2550 .and_then(|tab| match tab {
2551 SerializedTabRef::File(path) => {
2552 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2553 }
2554 _ => None,
2555 })
2556 .unwrap_or(0);
2557
2558 let mut file_states = HashMap::new();
2560 for (buffer_id, buf_state) in &view_state.keyed_states {
2561 let Some(meta) = buffer_metadata.get(buffer_id) else {
2562 continue;
2563 };
2564 let Some(abs_path) = meta.file_path() else {
2565 continue;
2566 };
2567
2568 let state_key = if abs_path.as_os_str().is_empty() {
2570 if let Some(ref recovery_id) = meta.recovery_id {
2572 PathBuf::from(format!("__unnamed__{}", recovery_id))
2573 } else {
2574 continue;
2575 }
2576 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2577 rp.to_path_buf()
2578 } else {
2579 abs_path.to_path_buf()
2581 };
2582
2583 let primary_cursor = buf_state.cursors.primary();
2584 let folds = buffers
2585 .get(buffer_id)
2586 .map(|state| {
2587 buf_state
2588 .folds
2589 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2590 .into_iter()
2591 .map(|range| SerializedFoldRange {
2592 header_line: range.header_line,
2593 end_line: range.end_line,
2594 placeholder: range.placeholder,
2595 header_text: range.header_text,
2596 })
2597 .collect::<Vec<_>>()
2598 })
2599 .unwrap_or_default();
2600
2601 file_states.insert(
2602 state_key,
2603 SerializedFileState {
2604 cursor: SerializedCursor {
2605 position: primary_cursor.position,
2606 anchor: primary_cursor.anchor,
2607 sticky_column: primary_cursor.sticky_column,
2608 },
2609 additional_cursors: buf_state
2610 .cursors
2611 .iter()
2612 .skip(1) .map(|(_, cursor)| SerializedCursor {
2614 position: cursor.position,
2615 anchor: cursor.anchor,
2616 sticky_column: cursor.sticky_column,
2617 })
2618 .collect(),
2619 scroll: SerializedScroll {
2620 top_byte: buf_state.viewport.top_byte,
2621 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2622 left_column: buf_state.viewport.left_column,
2623 },
2624 view_mode: match buf_state.view_mode {
2625 ViewMode::Source => SerializedViewMode::Source,
2626 ViewMode::PageView => SerializedViewMode::PageView,
2627 },
2628 compose_width: buf_state.compose_width,
2629 plugin_state: buf_state.plugin_state.clone(),
2630 folds,
2631 },
2632 );
2633 }
2634
2635 let active_view_mode = active_buffer
2637 .and_then(|id| view_state.keyed_states.get(&id))
2638 .map(|bs| match bs.view_mode {
2639 ViewMode::Source => SerializedViewMode::Source,
2640 ViewMode::PageView => SerializedViewMode::PageView,
2641 })
2642 .unwrap_or(SerializedViewMode::Source);
2643 let active_compose_width = active_buffer
2644 .and_then(|id| view_state.keyed_states.get(&id))
2645 .and_then(|bs| bs.compose_width);
2646
2647 SerializedSplitViewState {
2648 open_tabs,
2649 active_tab_index,
2650 open_files,
2651 active_file_index,
2652 file_states,
2653 tab_scroll_offset: view_state.tab_scroll_offset,
2654 view_mode: active_view_mode,
2655 compose_width: active_compose_width,
2656 }
2657}
2658
2659fn serialize_bookmarks(
2660 bookmarks: &BookmarkState,
2661 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2662 working_dir: &Path,
2663) -> HashMap<char, SerializedBookmark> {
2664 bookmarks
2665 .iter()
2666 .filter_map(|(key, bookmark)| {
2667 buffer_metadata
2668 .get(&bookmark.buffer_id)
2669 .and_then(|meta| meta.file_path())
2670 .and_then(|abs_path| {
2671 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2672 (
2673 key,
2674 SerializedBookmark {
2675 file_path: rel_path.to_path_buf(),
2676 position: bookmark.position,
2677 },
2678 )
2679 })
2680 })
2681 })
2682 .collect()
2683}
2684
2685fn collect_file_paths_from_states(
2687 split_states: &HashMap<usize, SerializedSplitViewState>,
2688) -> Vec<PathBuf> {
2689 let mut paths = Vec::new();
2690 for state in split_states.values() {
2691 if !state.open_tabs.is_empty() {
2692 for tab in &state.open_tabs {
2693 if let SerializedTabRef::File(path) = tab {
2694 if !paths.contains(path) {
2695 paths.push(path.clone());
2696 }
2697 }
2698 }
2699 } else {
2700 for path in &state.open_files {
2701 if !paths.contains(path) {
2702 paths.push(path.clone());
2703 }
2704 }
2705 }
2706 }
2707 paths
2708}
2709
2710fn get_expanded_dirs(
2712 explorer: &crate::view::file_tree::FileTreeView,
2713 working_dir: &Path,
2714) -> Vec<PathBuf> {
2715 let mut expanded = Vec::new();
2716 let tree = explorer.tree();
2717
2718 for node in tree.all_nodes() {
2720 if node.is_expanded() && node.is_dir() {
2721 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2723 expanded.push(rel_path.to_path_buf());
2724 }
2725 }
2726 }
2727
2728 expanded
2729}