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 terminal_id = match self.terminal_manager.spawn(
772 terminal.cols,
773 terminal.rows,
774 terminal.cwd.clone(),
775 Some(log_path.clone()),
776 Some(backing_path.clone()),
777 wrapper_for_spawn,
778 ) {
779 Ok(id) => id,
780 Err(e) => {
781 tracing::warn!(
782 "Failed to restore terminal {}: {}",
783 terminal.terminal_index,
784 e
785 );
786 return None;
787 }
788 };
789
790 if terminal_id != predicted_id {
792 self.terminal_log_files
793 .insert(terminal_id, log_path.clone());
794 self.terminal_backing_files
795 .insert(terminal_id, backing_path.clone());
796 self.terminal_log_files.remove(&predicted_id);
797 self.terminal_backing_files.remove(&predicted_id);
798 }
799
800 if let Some(argv) = terminal.command.as_ref() {
804 self.terminal_commands.insert(terminal_id, argv.clone());
805 }
806 if let Some(resume) = terminal.agent_resume.as_ref() {
807 if !resume.argv.is_empty() {
808 self.terminal_resume_commands
809 .insert(terminal_id, resume.argv.clone());
810 }
811 }
812
813 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
815
816 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
819
820 Some(buffer_id)
821 }
822
823 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
828 if !backing_path.exists() {
830 return;
831 }
832
833 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
834 if let Ok(new_state) = EditorState::from_file_with_languages(
835 backing_path,
836 self.terminal_width,
837 self.terminal_height,
838 large_file_threshold,
839 &self.resources.grammar_registry,
840 &self.resources.config.languages,
841 std::sync::Arc::clone(&self.authority().filesystem),
842 ) {
843 self.install_terminal_buffer_state(buffer_id, new_state);
844 }
845 }
846
847 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
849 for (buffer_id, metadata) in &self.buffer_metadata {
851 if let Some(file_path) = metadata.file_path() {
852 if file_path == path {
853 return Ok(*buffer_id);
854 }
855 }
856 }
857
858 self.open_file_no_focus(path).map_err(WorkspaceError::Io)
860 }
861
862 #[allow(clippy::too_many_arguments)]
864 fn restore_split_node(
865 &mut self,
866 node: &SerializedSplitNode,
867 path_to_buffer: &HashMap<PathBuf, BufferId>,
868 terminal_buffers: &HashMap<usize, BufferId>,
869 unnamed_buffers: &HashMap<String, BufferId>,
870 split_states: &HashMap<usize, SerializedSplitViewState>,
871 split_id_map: &mut HashMap<usize, SplitId>,
872 is_first_leaf: bool,
873 ) {
874 match node {
875 SerializedSplitNode::Leaf {
876 file_path,
877 split_id,
878 label,
879 unnamed_recovery_id,
880 role,
881 } => {
882 let buffer_id = file_path
884 .as_ref()
885 .and_then(|p| path_to_buffer.get(p).copied())
886 .or_else(|| {
887 unnamed_recovery_id
888 .as_ref()
889 .and_then(|id| unnamed_buffers.get(id).copied())
890 })
891 .unwrap_or(self.active_buffer());
892
893 let current_leaf_id = if is_first_leaf {
894 let leaf_id = self
896 .buffers
897 .splits()
898 .map(|(mgr, _)| mgr)
899 .expect("active window must have a populated split layout")
900 .active_split();
901 self.set_pane_buffer(leaf_id, buffer_id);
902 leaf_id
903 } else {
904 self.buffers
906 .splits()
907 .map(|(mgr, _)| mgr)
908 .expect("active window must have a populated split layout")
909 .active_split()
910 };
911
912 split_id_map.insert(*split_id, current_leaf_id.into());
914
915 if let Some(label) = label {
917 self.buffers
918 .split_manager_mut()
919 .expect("active window must have a populated split layout")
920 .set_label(current_leaf_id, label.clone());
921 }
922
923 if let Some(role) = role {
926 self.buffers
927 .split_manager_mut()
928 .expect("active window must have a populated split layout")
929 .clear_role(*role);
930 self.buffers
931 .split_manager_mut()
932 .expect("active window must have a populated split layout")
933 .set_leaf_role(current_leaf_id, Some(*role));
934 }
935
936 self.restore_split_view_state(
938 current_leaf_id,
939 *split_id,
940 split_states,
941 path_to_buffer,
942 terminal_buffers,
943 unnamed_buffers,
944 );
945 }
946 SerializedSplitNode::Terminal {
947 terminal_index,
948 split_id,
949 label,
950 role,
951 } => {
952 let buffer_id = terminal_buffers
953 .get(terminal_index)
954 .copied()
955 .unwrap_or(self.active_buffer());
956
957 let current_leaf_id = if is_first_leaf {
958 let leaf_id = self
959 .buffers
960 .splits()
961 .map(|(mgr, _)| mgr)
962 .expect("active window must have a populated split layout")
963 .active_split();
964 self.set_pane_buffer(leaf_id, buffer_id);
965 leaf_id
966 } else {
967 self.buffers
968 .splits()
969 .map(|(mgr, _)| mgr)
970 .expect("active window must have a populated split layout")
971 .active_split()
972 };
973
974 split_id_map.insert(*split_id, current_leaf_id.into());
975
976 if let Some(label) = label {
978 self.buffers
979 .split_manager_mut()
980 .expect("active window must have a populated split layout")
981 .set_label(current_leaf_id, label.clone());
982 }
983
984 if let Some(role) = role {
987 self.buffers
988 .split_manager_mut()
989 .expect("active window must have a populated split layout")
990 .clear_role(*role);
991 self.buffers
992 .split_manager_mut()
993 .expect("active window must have a populated split layout")
994 .set_leaf_role(current_leaf_id, Some(*role));
995 }
996
997 self.buffers
998 .split_manager_mut()
999 .expect("active window must have a populated split layout")
1000 .set_split_buffer(current_leaf_id, buffer_id);
1001
1002 self.restore_split_view_state(
1003 current_leaf_id,
1004 *split_id,
1005 split_states,
1006 path_to_buffer,
1007 terminal_buffers,
1008 unnamed_buffers,
1009 );
1010 }
1011 SerializedSplitNode::Split {
1012 direction,
1013 first,
1014 second,
1015 ratio,
1016 split_id,
1017 } => {
1018 self.restore_split_node(
1020 first,
1021 path_to_buffer,
1022 terminal_buffers,
1023 unnamed_buffers,
1024 split_states,
1025 split_id_map,
1026 is_first_leaf,
1027 );
1028
1029 let second_buffer_id = get_first_leaf_buffer(
1031 second,
1032 path_to_buffer,
1033 terminal_buffers,
1034 unnamed_buffers,
1035 )
1036 .unwrap_or(self.active_buffer());
1037
1038 let split_direction = match direction {
1040 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1041 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1042 };
1043
1044 match self
1046 .buffers
1047 .split_manager_mut()
1048 .expect("active window must have a populated split layout")
1049 .split_active(split_direction, second_buffer_id, *ratio)
1050 {
1051 Ok(new_leaf_id) => {
1052 let mut view_state = SplitViewState::with_buffer(
1054 self.terminal_width,
1055 self.terminal_height,
1056 second_buffer_id,
1057 );
1058 view_state.apply_config_defaults(
1059 self.resources.config.editor.line_numbers,
1060 self.resources.config.editor.highlight_current_line,
1061 self.resolve_line_wrap_for_buffer(second_buffer_id),
1062 self.resources.config.editor.wrap_indent,
1063 self.resolve_wrap_column_for_buffer(second_buffer_id),
1064 self.resources.config.editor.rulers.clone(),
1065 self.resources.config.editor.scroll_offset,
1066 );
1067 self.buffers
1068 .split_view_states_mut()
1069 .expect("active window must have a populated split layout")
1070 .insert(new_leaf_id, view_state);
1071
1072 split_id_map.insert(*split_id, new_leaf_id.into());
1074
1075 self.restore_split_node(
1077 second,
1078 path_to_buffer,
1079 terminal_buffers,
1080 unnamed_buffers,
1081 split_states,
1082 split_id_map,
1083 false,
1084 );
1085 }
1086 Err(e) => {
1087 tracing::error!("Failed to create split during workspace restore: {}", e);
1088 }
1089 }
1090 }
1091 }
1092 }
1093
1094 fn restore_split_view_state(
1096 &mut self,
1097 current_split_id: LeafId,
1098 saved_split_id: usize,
1099 split_states: &HashMap<usize, SerializedSplitViewState>,
1100 path_to_buffer: &HashMap<PathBuf, BufferId>,
1101 terminal_buffers: &HashMap<usize, BufferId>,
1102 unnamed_buffers: &HashMap<String, BufferId>,
1103 ) {
1104 let Some(split_state) = split_states.get(&saved_split_id) else {
1106 return;
1107 };
1108
1109 let split_buf_for_current = self
1113 .buffers
1114 .split_manager()
1115 .expect("active window must have a populated split layout")
1116 .buffer_for_split(current_split_id);
1117 let active_buffer_id = self
1118 .buffers
1119 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1120 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1121 return None;
1122 };
1123 let mut active_buffer_id: Option<BufferId> = None;
1124 if !split_state.open_tabs.is_empty() {
1125 view_state.open_buffers.clear();
1128
1129 for tab in &split_state.open_tabs {
1130 match tab {
1131 SerializedTabRef::File(rel_path) => {
1132 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1133 if !view_state.has_buffer(buffer_id) {
1134 view_state.add_buffer(buffer_id);
1135 }
1136 view_state.ensure_buffer_state(buffer_id);
1138 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1139 let buf_state =
1140 view_state.buffer_state_mut(buffer_id).unwrap();
1141 buf_state.viewport.line_wrap_enabled = false;
1142 buf_state.show_line_numbers = false;
1146 buf_state.highlight_current_line = false;
1147 }
1148 }
1149 }
1150 SerializedTabRef::Terminal(index) => {
1151 if let Some(&buffer_id) = terminal_buffers.get(index) {
1152 if !view_state.has_buffer(buffer_id) {
1153 view_state.add_buffer(buffer_id);
1154 }
1155 let buf_state = view_state.ensure_buffer_state(buffer_id);
1156 buf_state.viewport.line_wrap_enabled = false;
1157 buf_state.show_line_numbers = false;
1161 buf_state.highlight_current_line = false;
1162 }
1163 }
1164 SerializedTabRef::Unnamed(recovery_id) => {
1165 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1166 if !view_state.has_buffer(buffer_id) {
1167 view_state.add_buffer(buffer_id);
1168 }
1169 view_state.ensure_buffer_state(buffer_id);
1170 }
1171 }
1172 }
1173 }
1174
1175 if view_state.open_buffers.is_empty() {
1180 if let Some(buf) = split_buf_for_current {
1181 view_state.add_buffer(buf);
1182 view_state.ensure_buffer_state(buf);
1183 }
1184 }
1185
1186 if let Some(active_idx) = split_state.active_tab_index {
1187 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1188 active_buffer_id = match tab {
1189 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1190 SerializedTabRef::Terminal(index) => {
1191 terminal_buffers.get(index).copied()
1192 }
1193 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1194 };
1195 }
1196 }
1197 } else {
1198 for rel_path in &split_state.open_files {
1200 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1201 if !view_state.has_buffer(buffer_id) {
1202 view_state.add_buffer(buffer_id);
1203 }
1204 view_state.ensure_buffer_state(buffer_id);
1205 }
1206 }
1207
1208 let active_file_path =
1209 split_state.open_files.get(split_state.active_file_index);
1210 active_buffer_id =
1211 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1212 }
1213
1214 for (rel_path, file_state) in &split_state.file_states {
1216 let rel_str = rel_path.to_string_lossy();
1218 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1219 match unnamed_buffers.get(recovery_id).copied() {
1220 Some(id) => id,
1221 None => continue,
1222 }
1223 } else {
1224 match path_to_buffer.get(rel_path).copied() {
1225 Some(id) => id,
1226 None => continue,
1227 }
1228 };
1229 let max_pos = __buffers_mut
1230 .get(&buffer_id)
1231 .map(|b| b.buffer.len())
1232 .unwrap_or(0);
1233
1234 let buf_state = view_state.ensure_buffer_state(buffer_id);
1236
1237 let cursor_pos = file_state.cursor.position.min(max_pos);
1238 buf_state.cursors.primary_mut().position = cursor_pos;
1239 buf_state.cursors.primary_mut().anchor =
1240 file_state.cursor.anchor.map(|a| a.min(max_pos));
1241 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1242
1243 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1244 buf_state.viewport.top_view_line_offset =
1245 file_state.scroll.top_view_line_offset;
1246 buf_state.viewport.left_column = file_state.scroll.left_column;
1247 buf_state.viewport.set_skip_resize_sync();
1248
1249 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1257 super::navigation::reconcile_restored_buffer_view(
1258 buf_state,
1259 &mut state.buffer,
1260 );
1261 }
1262
1263 buf_state.view_mode = match file_state.view_mode {
1265 SerializedViewMode::Source => ViewMode::Source,
1266 SerializedViewMode::PageView => ViewMode::PageView,
1267 };
1268 buf_state.compose_width = file_state.compose_width;
1269 buf_state.plugin_state = file_state.plugin_state.clone();
1270 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1271 buf_state.folds.clear(&mut state.marker_list);
1272 for fold in &file_state.folds {
1273 let Some(resolved_header) = resolve_fold_header_line(
1280 &state.buffer,
1281 fold.header_line,
1282 fold.header_text.as_deref(),
1283 ) else {
1284 tracing::debug!(
1285 "Dropping stale fold: header_line={} no longer matches stored \
1286 header_text after external edit",
1287 fold.header_line,
1288 );
1289 continue;
1290 };
1291
1292 let shift = resolved_header as i64 - fold.header_line as i64;
1294 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1295 let start_line = resolved_header.saturating_add(1);
1296 let end_line = adjusted_end;
1297 if start_line > end_line {
1298 continue;
1299 }
1300 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1301 else {
1302 continue;
1303 };
1304 let end_byte = state
1305 .buffer
1306 .line_start_offset(end_line.saturating_add(1))
1307 .unwrap_or_else(|| state.buffer.len());
1308 buf_state.folds.add(
1309 &mut state.marker_list,
1310 start_byte,
1311 end_byte,
1312 fold.placeholder.clone(),
1313 );
1314 }
1315 }
1316
1317 tracing::trace!(
1318 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1319 rel_path,
1320 cursor_pos,
1321 buf_state.viewport.top_byte,
1322 buf_state.view_mode,
1323 );
1324 }
1325
1326 if active_buffer_id.is_none() {
1343 active_buffer_id = view_state.buffer_tab_ids().next();
1344 }
1345
1346 let restored_view_mode = match split_state.view_mode {
1349 SerializedViewMode::Source => ViewMode::Source,
1350 SerializedViewMode::PageView => ViewMode::PageView,
1351 };
1352
1353 if let Some(active_buf_id) = active_buffer_id {
1354 view_state.switch_buffer(active_buf_id);
1356
1357 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
1359 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
1360 });
1361 if !active_has_file_state {
1362 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1363 view_state.active_state_mut().compose_width = split_state.compose_width;
1364 }
1365
1366 }
1368 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1369 active_buffer_id
1370 })
1371 .flatten();
1372
1373 if let Some(active_buf_id) = active_buffer_id {
1377 self.buffers
1378 .split_manager_mut()
1379 .expect("active window must have a populated split layout")
1380 .set_split_buffer(current_split_id, active_buf_id);
1381 }
1382 }
1383
1384 fn restore_search_options(&mut self, opts: &SearchOptions) {
1385 self.search_case_sensitive = opts.case_sensitive;
1386 self.search_whole_word = opts.whole_word;
1387 self.search_use_regex = opts.use_regex;
1388 self.search_confirm_each = opts.confirm_each;
1389 }
1390
1391 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
1392 tracing::debug!(
1393 "Restoring histories: {} search, {} replace, {} goto_line",
1394 histories.search.len(),
1395 histories.replace.len(),
1396 histories.goto_line.len()
1397 );
1398 for item in &histories.search {
1399 self.prompt_histories
1400 .entry("search".to_string())
1401 .or_default()
1402 .push(item.clone());
1403 }
1404 for item in &histories.replace {
1405 self.prompt_histories
1406 .entry("replace".to_string())
1407 .or_default()
1408 .push(item.clone());
1409 }
1410 for item in &histories.goto_line {
1411 self.prompt_histories
1412 .entry("goto_line".to_string())
1413 .or_default()
1414 .push(item.clone());
1415 }
1416 }
1417
1418 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
1419 self.file_explorer_visible = fe.visible;
1420 self.file_explorer_width = fe.width;
1421 self.file_explorer_side = fe.side;
1422
1423 if fe.show_hidden {
1425 self.pending_file_explorer_show_hidden = Some(true);
1426 }
1427 if fe.show_gitignored {
1428 self.pending_file_explorer_show_gitignored = Some(true);
1429 }
1430
1431 if self.file_explorer_visible && self.file_explorer.is_none() {
1433 self.init_file_explorer();
1434 }
1435 }
1436
1437 fn open_workspace_files(
1440 &mut self,
1441 split_states: &HashMap<usize, SerializedSplitViewState>,
1442 ) -> HashMap<PathBuf, BufferId> {
1443 let file_paths = collect_file_paths_from_states(split_states);
1444 tracing::debug!(
1445 "Workspace has {} files to restore: {:?}",
1446 file_paths.len(),
1447 file_paths
1448 );
1449 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
1450 for rel_path in file_paths {
1451 let abs_path = self.root.join(&rel_path);
1452 tracing::trace!(
1453 "Checking file: {:?} (exists: {})",
1454 abs_path,
1455 abs_path.exists()
1456 );
1457 if abs_path.exists() {
1458 match self.open_file_internal(&abs_path) {
1459 Ok(buffer_id) => {
1460 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
1461 path_to_buffer.insert(rel_path, buffer_id);
1462 }
1463 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
1464 }
1465 } else {
1466 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
1467 }
1468 }
1469 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
1470 path_to_buffer
1471 }
1472
1473 fn restore_external_files(
1475 &mut self,
1476 external_files: &[PathBuf],
1477 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
1478 ) {
1479 if external_files.is_empty() {
1480 return;
1481 }
1482 tracing::debug!(
1483 "Restoring {} external files: {:?}",
1484 external_files.len(),
1485 external_files
1486 );
1487 for abs_path in external_files {
1488 if !abs_path.exists() {
1489 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
1490 continue;
1491 }
1492 match self.open_file_internal(abs_path) {
1493 Ok(buffer_id) => {
1494 path_to_buffer.insert(abs_path.clone(), buffer_id);
1495 tracing::debug!(
1496 "Restored external file {:?} as buffer {:?}",
1497 abs_path,
1498 buffer_id
1499 );
1500 }
1501 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1502 }
1503 }
1504 }
1505
1506 fn apply_read_only_flags(
1509 &mut self,
1510 read_only_files: &[PathBuf],
1511 path_to_buffer: &HashMap<PathBuf, BufferId>,
1512 ) {
1513 for ro_path in read_only_files {
1514 let buffer_id = path_to_buffer
1515 .get(ro_path)
1516 .copied()
1517 .or_else(|| path_to_buffer.get(&self.root.join(ro_path)).copied());
1518 if let Some(id) = buffer_id {
1519 self.mark_buffer_read_only(id, true);
1520 }
1521 }
1522 }
1523
1524 pub(crate) fn has_any_virtual_buffer(&self) -> bool {
1529 self.buffer_metadata
1530 .values()
1531 .any(|m| matches!(m.kind, crate::app::types::BufferKind::Virtual { .. }))
1532 }
1533
1534 pub(crate) fn save_all_global_file_states(&self) {
1537 for (leaf_id, view_state) in self
1538 .buffers
1539 .splits()
1540 .map(|(_, vs)| vs)
1541 .expect("window must have a populated split layout")
1542 {
1543 let active_buffer = self
1544 .buffers
1545 .splits()
1546 .map(|(mgr, _)| mgr)
1547 .expect("window must have a populated split layout")
1548 .root()
1549 .get_leaves_with_rects(ratatui::layout::Rect::default())
1550 .into_iter()
1551 .find(|(sid, _, _)| *sid == *leaf_id)
1552 .map(|(_, buffer_id, _)| buffer_id);
1553
1554 if let Some(buffer_id) = active_buffer {
1555 self.save_buffer_file_state(buffer_id, view_state);
1556 }
1557 }
1558 }
1559
1560 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
1562 let abs_path = match self.buffer_metadata.get(&buffer_id) {
1563 Some(metadata) => match metadata.file_path() {
1564 Some(path) => path.to_path_buf(),
1565 None => return,
1566 },
1567 None => return,
1568 };
1569
1570 let primary_cursor = view_state.cursors.primary();
1571 let file_state = SerializedFileState {
1572 cursor: SerializedCursor {
1573 position: primary_cursor.position,
1574 anchor: primary_cursor.anchor,
1575 sticky_column: primary_cursor.sticky_column,
1576 },
1577 additional_cursors: view_state
1578 .cursors
1579 .iter()
1580 .skip(1)
1581 .map(|(_, cursor)| SerializedCursor {
1582 position: cursor.position,
1583 anchor: cursor.anchor,
1584 sticky_column: cursor.sticky_column,
1585 })
1586 .collect(),
1587 scroll: SerializedScroll {
1588 top_byte: view_state.viewport.top_byte,
1589 top_view_line_offset: view_state.viewport.top_view_line_offset,
1590 left_column: view_state.viewport.left_column,
1591 },
1592 view_mode: Default::default(),
1593 compose_width: None,
1594 plugin_state: std::collections::HashMap::new(),
1595 folds: Vec::new(),
1596 };
1597
1598 PersistedFileWorkspace::save(&abs_path, file_state);
1599 }
1600
1601 pub(crate) fn sync_terminal_backing_files(&self) {
1604 use std::io::BufWriter;
1605
1606 let terminals_to_sync: Vec<_> = self
1607 .terminal_buffers
1608 .values()
1609 .copied()
1610 .filter_map(|terminal_id| {
1611 self.terminal_backing_files
1612 .get(&terminal_id)
1613 .map(|path| (terminal_id, path.clone()))
1614 })
1615 .collect();
1616
1617 for (terminal_id, backing_path) in terminals_to_sync {
1618 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1619 if let Ok(mut state) = handle.state.lock() {
1620 if let Ok(mut file) = self
1625 .authority()
1626 .filesystem
1627 .open_file_for_append(&backing_path)
1628 {
1629 let mut writer = BufWriter::new(&mut *file);
1630 if let Err(e) = state.flush_new_scrollback(&mut writer) {
1631 tracing::warn!(
1632 "Failed to flush terminal {:?} scrollback: {}",
1633 terminal_id,
1634 e
1635 );
1636 }
1637 }
1638
1639 if let Ok(mut file) = self
1640 .authority()
1641 .filesystem
1642 .open_file_for_append(&backing_path)
1643 {
1644 let mut writer = BufWriter::new(&mut *file);
1645 if let Err(e) = state.append_visible_screen(&mut writer) {
1646 tracing::warn!(
1647 "Failed to sync terminal {:?} to backing file: {}",
1648 terminal_id,
1649 e
1650 );
1651 }
1652 }
1653 }
1654 }
1655 }
1656 }
1657
1658 pub(crate) fn create_unnamed_recovery_buffer(
1662 &mut self,
1663 text: &str,
1664 recovery_id: String,
1665 display_name: String,
1666 ) -> BufferId {
1667 let buffer_id = self.alloc_buffer_id();
1668 let mut state = EditorState::new(
1669 self.terminal_width,
1670 self.terminal_height,
1671 self.resources.config.editor.large_file_threshold_bytes as usize,
1672 std::sync::Arc::clone(&self.authority().filesystem),
1673 );
1674 state
1675 .margins
1676 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1677 state.buffer.set_default_line_ending(
1678 self.resources
1679 .config
1680 .editor
1681 .default_line_ending
1682 .to_line_ending(),
1683 );
1684 state.buffer.insert(0, text);
1685 state.buffer.set_modified(true);
1686 state.buffer.set_recovery_pending(false);
1687 self.buffers.insert(buffer_id, state);
1688
1689 let mut log = crate::model::event::EventLog::new();
1690 log.clear_saved_position();
1691 self.event_logs.insert(buffer_id, log);
1692
1693 let mut meta = crate::app::types::BufferMetadata::new();
1694 meta.recovery_id = Some(recovery_id);
1695 meta.display_name = display_name;
1696 self.buffer_metadata.insert(buffer_id, meta);
1697
1698 buffer_id
1699 }
1700
1701 pub(crate) fn seed_initial_layout(&mut self) {
1705 if self.buffers.splits().is_some() && self.buffers.len() > 0 {
1706 return;
1707 }
1708 let buf = self.alloc_buffer_id();
1709 let mut state = EditorState::new(
1710 self.terminal_width,
1711 self.terminal_height,
1712 self.resources.config.editor.large_file_threshold_bytes as usize,
1713 std::sync::Arc::clone(&self.authority().filesystem),
1714 );
1715 state
1716 .margins
1717 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1718 state.buffer.set_default_line_ending(
1719 self.resources
1720 .config
1721 .editor
1722 .default_line_ending
1723 .to_line_ending(),
1724 );
1725 let manager = crate::view::split::SplitManager::new(buf);
1726 let active_leaf = manager.active_split();
1727 let mut view_states = HashMap::new();
1728 view_states.insert(
1729 active_leaf,
1730 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buf),
1731 );
1732 self.buffers.set_splits((manager, view_states));
1733 self.buffers.insert(buf, state);
1734 self.buffer_metadata
1735 .insert(buf, crate::app::types::BufferMetadata::new());
1736 self.event_logs
1737 .insert(buf, crate::model::event::EventLog::new());
1738 }
1739
1740 pub(crate) fn sync_lsp_after_recovery_replay(&mut self, buffer_id: BufferId) {
1744 let Some(text) = self
1745 .buffers
1746 .get(&buffer_id)
1747 .and_then(|state| state.buffer.to_string())
1748 else {
1749 return;
1750 };
1751 let full_change = lsp_types::TextDocumentContentChangeEvent {
1752 range: None,
1753 range_length: None,
1754 text,
1755 };
1756 self.send_lsp_changes_for_buffer(buffer_id, vec![full_change]);
1757 }
1758
1759 fn restore_unnamed_buffers(
1765 &mut self,
1766 unnamed_buffers: &[UnnamedBufferRef],
1767 ) -> HashMap<String, BufferId> {
1768 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1769 if !self.resources.config.editor.hot_exit || unnamed_buffers.is_empty() {
1770 return unnamed_buffer_map;
1771 }
1772 tracing::debug!(
1773 "Restoring {} unnamed buffers from recovery",
1774 unnamed_buffers.len()
1775 );
1776 for unnamed_ref in unnamed_buffers {
1777 let entries = match self
1778 .resources
1779 .recovery_service
1780 .lock()
1781 .unwrap()
1782 .list_recoverable()
1783 {
1784 Ok(e) => e,
1785 Err(e) => {
1786 tracing::warn!("Failed to list recovery entries: {}", e);
1787 continue;
1788 }
1789 };
1790 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1791 tracing::debug!(
1792 "Recovery file not found for unnamed buffer {}",
1793 unnamed_ref.recovery_id
1794 );
1795 continue;
1796 };
1797 let loaded = self
1798 .resources
1799 .recovery_service
1800 .lock()
1801 .unwrap()
1802 .load_recovery(entry);
1803 match loaded {
1804 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1805 let text = String::from_utf8_lossy(&content).into_owned();
1806 let buffer_id = self.create_unnamed_recovery_buffer(
1807 &text,
1808 unnamed_ref.recovery_id.clone(),
1809 unnamed_ref.display_name.clone(),
1810 );
1811 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1812 tracing::info!(
1813 "Restored unnamed buffer '{}' (recovery_id={})",
1814 unnamed_ref.display_name,
1815 unnamed_ref.recovery_id
1816 );
1817 }
1818 Ok(other) => {
1819 tracing::warn!(
1820 "Unexpected recovery result for unnamed buffer {}: {:?}",
1821 unnamed_ref.recovery_id,
1822 std::mem::discriminant(&other)
1823 );
1824 }
1825 Err(e) => {
1826 tracing::warn!(
1827 "Failed to load recovery for unnamed buffer {}: {}",
1828 unnamed_ref.recovery_id,
1829 e
1830 );
1831 }
1832 }
1833 }
1834 unnamed_buffer_map
1835 }
1836
1837 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1841 if !self.resources.config.editor.hot_exit {
1842 return;
1843 }
1844 let entries = self
1845 .resources
1846 .recovery_service
1847 .lock()
1848 .unwrap()
1849 .list_recoverable()
1850 .unwrap_or_default();
1851 if entries.is_empty() {
1852 return;
1853 }
1854 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1855 for buffer_id in buffer_ids {
1856 let file_path = self
1857 .buffers
1858 .get(&buffer_id)
1859 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1860 let Some(file_path) = file_path else { continue };
1861
1862 let recovery_id = self
1863 .resources
1864 .recovery_service
1865 .lock()
1866 .unwrap()
1867 .get_buffer_id(Some(&file_path));
1868 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1869 continue;
1870 };
1871 let loaded = self
1872 .resources
1873 .recovery_service
1874 .lock()
1875 .unwrap()
1876 .load_recovery(entry);
1877 match loaded {
1878 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1879 let mut mutated = false;
1880 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1881 let current_len = state.buffer.total_bytes();
1882 let text = String::from_utf8_lossy(&content).into_owned();
1883 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1884 let current_text = current
1885 .as_ref()
1886 .map(|b| String::from_utf8_lossy(b).into_owned());
1887 if current_text.as_deref() != Some(&text) {
1888 state.buffer.delete(0..current_len);
1889 state.buffer.insert(0, &text);
1890 state.buffer.set_modified(true);
1891 state.buffer.set_recovery_pending(false);
1892 mutated = true;
1893 tracing::info!(
1894 "Restored unsaved changes for {:?} from hot exit recovery",
1895 file_path
1896 );
1897 }
1898 }
1899 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1900 log.clear_saved_position();
1901 }
1902 if mutated {
1903 self.sync_lsp_after_recovery_replay(buffer_id);
1904 }
1905 }
1906 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
1907 chunks, ..
1908 }) => {
1909 let mut mutated = false;
1910 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1911 for chunk in chunks.into_iter().rev() {
1912 let text = String::from_utf8_lossy(&chunk.content).into_owned();
1913 if chunk.original_len > 0 {
1914 state
1915 .buffer
1916 .delete(chunk.offset..chunk.offset + chunk.original_len);
1917 }
1918 state.buffer.insert(chunk.offset, &text);
1919 }
1920 state.buffer.set_modified(true);
1921 state.buffer.set_recovery_pending(false);
1922 mutated = true;
1923 tracing::info!(
1924 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
1925 file_path
1926 );
1927 }
1928 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
1929 log.clear_saved_position();
1930 }
1931 if mutated {
1932 self.sync_lsp_after_recovery_replay(buffer_id);
1933 }
1934 }
1935 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1936 original_path,
1937 ..
1938 }) => {
1939 let name = original_path
1940 .file_name()
1941 .unwrap_or_default()
1942 .to_string_lossy();
1943 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1944 self.set_status_message(format!(
1945 "{} changed on disk; unsaved changes not restored",
1946 name
1947 ));
1948 }
1949 Ok(_) => {} Err(e) => {
1951 tracing::debug!(
1952 "Failed to load hot exit recovery for {:?}: {}",
1953 file_path,
1954 e
1955 );
1956 }
1957 }
1958 }
1959 }
1960
1961 pub(crate) fn apply_workspace_layout(
1977 &mut self,
1978 workspace: &Workspace,
1979 session_name: Option<&str>,
1980 ) {
1981 tracing::debug!(
1982 "Applying workspace layout with {} split states",
1983 workspace.split_states.len()
1984 );
1985
1986 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
1989 self.mouse_enabled = mouse_enabled;
1990 }
1991
1992 self.restore_search_options(&workspace.search_options);
1993 self.restore_prompt_histories(&workspace.histories);
1994 self.restore_file_explorer_settings(&workspace.file_explorer);
1995
1996 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
1999
2000 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
2001 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
2002 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
2003
2004 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
2005
2006 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
2007 self.restore_split_node(
2008 &workspace.split_layout,
2009 &path_to_buffer,
2010 &terminal_buffer_map,
2011 &unnamed_buffer_map,
2012 &workspace.split_states,
2013 &mut split_id_map,
2014 true,
2015 );
2016
2017 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
2018 self.buffers
2019 .split_manager_mut()
2020 .expect("window must have a populated split layout")
2021 .set_active_split(LeafId(new_active_split));
2022 }
2023
2024 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
2025 self.clean_orphaned_buffers();
2026 self.log_restore_summary(session_name);
2027
2028 self.restore_hot_exit_changes(&path_to_buffer);
2030 }
2031
2032 pub(crate) fn from_workspace(
2038 id: fresh_core::WindowId,
2039 label: impl Into<String>,
2040 root: PathBuf,
2041 authority: crate::services::authority::Authority,
2042 resources: crate::app::window_resources::WindowResources,
2043 workspace: &Workspace,
2044 ) -> Self {
2045 let mut window = Self::new(id, label, root, authority, resources);
2046 window.seed_initial_layout();
2047 window.apply_workspace_layout(workspace, None);
2048 window
2049 }
2050
2051 pub(crate) fn capture_workspace(&self) -> Workspace {
2057 tracing::debug!("Capturing workspace for {:?}", self.root);
2058
2059 let mut terminals = Vec::new();
2060 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
2061 let mut seen = HashSet::new();
2062 for terminal_id in self.terminal_buffers.values().copied() {
2063 if seen.insert(terminal_id) {
2064 let command = self.terminal_commands.get(&terminal_id).cloned();
2065 if self.ephemeral_terminals.contains(&terminal_id) && command.is_none() {
2073 continue;
2074 }
2075 let idx = terminals.len();
2076 terminal_indices.insert(terminal_id, idx);
2077 let handle = self.terminal_manager.get(terminal_id);
2078 let (cols, rows) = handle
2079 .map(|h| h.size())
2080 .unwrap_or((self.terminal_width, self.terminal_height));
2081 let cwd = handle.and_then(|h| h.cwd());
2082 let shell = handle
2083 .map(|h| h.shell().to_string())
2084 .unwrap_or_else(crate::services::terminal::detect_shell);
2085 let log_path = self
2086 .terminal_log_files
2087 .get(&terminal_id)
2088 .cloned()
2089 .unwrap_or_else(|| {
2090 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2091 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
2092 });
2093 let backing_path = self
2094 .terminal_backing_files
2095 .get(&terminal_id)
2096 .cloned()
2097 .unwrap_or_else(|| {
2098 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2099 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
2100 });
2101
2102 let agent_resume = self
2103 .terminal_resume_commands
2104 .get(&terminal_id)
2105 .filter(|argv| !argv.is_empty())
2106 .map(|argv| crate::workspace::AgentResume { argv: argv.clone() });
2107 terminals.push(SerializedTerminalWorkspace {
2108 terminal_index: idx,
2109 cwd,
2110 shell,
2111 cols,
2112 rows,
2113 log_path,
2114 backing_path,
2115 command,
2116 agent_resume,
2117 });
2118 }
2119 }
2120
2121 let (mgr, view_states) = self
2122 .buffers
2123 .splits()
2124 .expect("window must have a populated split layout");
2125
2126 let split_layout = serialize_split_node(
2127 mgr.root(),
2128 &self.buffer_metadata,
2129 &self.root,
2130 &self.terminal_buffers,
2131 &terminal_indices,
2132 mgr.labels(),
2133 );
2134
2135 let active_buffers: HashMap<LeafId, BufferId> = mgr
2136 .root()
2137 .get_leaves_with_rects(ratatui::layout::Rect::default())
2138 .into_iter()
2139 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
2140 .collect();
2141
2142 let mut split_states = HashMap::new();
2143 for (leaf_id, view_state) in view_states {
2144 let active_buffer = active_buffers.get(leaf_id).copied();
2145 let serialized = serialize_split_view_state(
2146 view_state,
2147 self.buffers.as_map(),
2148 &self.buffer_metadata,
2149 &self.root,
2150 active_buffer,
2151 &self.terminal_buffers,
2152 &terminal_indices,
2153 );
2154 split_states.insert(leaf_id.0 .0, serialized);
2155 }
2156
2157 let file_explorer = if let Some(explorer) = self.file_explorer.as_ref() {
2158 let expanded_dirs = get_expanded_dirs(explorer, &self.root);
2159 FileExplorerState {
2160 visible: self.file_explorer_visible,
2161 width: self.file_explorer_width,
2162 side: self.file_explorer_side,
2163 expanded_dirs,
2164 scroll_offset: explorer.get_scroll_offset(),
2165 show_hidden: explorer.ignore_patterns().show_hidden(),
2166 show_gitignored: explorer.ignore_patterns().show_gitignored(),
2167 }
2168 } else {
2169 FileExplorerState {
2170 visible: self.file_explorer_visible,
2171 width: self.file_explorer_width,
2172 side: self.file_explorer_side,
2173 expanded_dirs: Vec::new(),
2174 scroll_offset: 0,
2175 show_hidden: false,
2176 show_gitignored: false,
2177 }
2178 };
2179
2180 let cfg = &self.resources.config.editor;
2181 let config_overrides = WorkspaceConfigOverrides {
2182 line_numbers: Some(cfg.line_numbers),
2183 relative_line_numbers: Some(cfg.relative_line_numbers),
2184 line_wrap: Some(cfg.line_wrap),
2185 syntax_highlighting: Some(cfg.syntax_highlighting),
2186 enable_inlay_hints: Some(cfg.enable_inlay_hints),
2187 mouse_enabled: Some(self.mouse_enabled),
2188 menu_bar_hidden: None,
2189 };
2190
2191 let histories = WorkspaceHistories {
2192 search: self
2193 .prompt_histories
2194 .get("search")
2195 .map(|h| h.items().to_vec())
2196 .unwrap_or_default(),
2197 replace: self
2198 .prompt_histories
2199 .get("replace")
2200 .map(|h| h.items().to_vec())
2201 .unwrap_or_default(),
2202 command_palette: Vec::new(),
2203 goto_line: self
2204 .prompt_histories
2205 .get("goto_line")
2206 .map(|h| h.items().to_vec())
2207 .unwrap_or_default(),
2208 open_file: Vec::new(),
2209 };
2210
2211 let search_options = SearchOptions {
2212 case_sensitive: self.search_case_sensitive,
2213 whole_word: self.search_whole_word,
2214 use_regex: self.search_use_regex,
2215 confirm_each: self.search_confirm_each,
2216 };
2217
2218 let bookmarks = serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.root);
2219
2220 let external_files: Vec<PathBuf> = self
2221 .buffer_metadata
2222 .values()
2223 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2224 .filter_map(|meta| meta.file_path())
2225 .filter(|abs_path| abs_path.strip_prefix(&self.root).is_err())
2226 .cloned()
2227 .collect();
2228
2229 let read_only_files: Vec<PathBuf> = self
2230 .buffer_metadata
2231 .values()
2232 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2233 .filter(|meta| meta.read_only)
2234 .filter_map(|meta| meta.file_path().cloned())
2235 .filter(|p| !p.as_os_str().is_empty())
2236 .map(|p| {
2237 p.strip_prefix(&self.root)
2238 .map(|rel| rel.to_path_buf())
2239 .unwrap_or(p)
2240 })
2241 .collect();
2242
2243 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.resources.config.editor.hot_exit {
2244 self.buffer_metadata
2245 .iter()
2246 .filter_map(|(buffer_id, meta)| {
2247 let path = meta.file_path()?;
2248 if !path.as_os_str().is_empty() {
2249 return None;
2250 }
2251 if meta.hidden_from_tabs || meta.is_virtual() {
2252 return None;
2253 }
2254 let state = self.buffers.get(buffer_id)?;
2255 if state.buffer.total_bytes() == 0 {
2256 return None;
2257 }
2258 let recovery_id = meta.recovery_id.clone()?;
2259 Some(UnnamedBufferRef {
2260 recovery_id,
2261 display_name: meta.display_name.clone(),
2262 })
2263 })
2264 .collect()
2265 } else {
2266 Vec::new()
2267 };
2268
2269 Workspace {
2270 version: WORKSPACE_VERSION,
2271 working_dir: self.root.clone(),
2272 split_layout,
2273 active_split_id: SplitId::from(mgr.active_split()).0,
2274 split_states,
2275 config_overrides,
2276 file_explorer,
2277 histories,
2278 search_options,
2279 bookmarks,
2280 terminals,
2281 external_files,
2282 read_only_files,
2283 unnamed_buffers,
2284 plugin_global_state: HashMap::new(),
2285 saved_at: std::time::SystemTime::now()
2286 .duration_since(std::time::UNIX_EPOCH)
2287 .unwrap_or_default()
2288 .as_secs(),
2289 label: Some(self.label.clone()),
2292 session_plugin_state: self.plugin_state.clone(),
2293 authority_spec: self.authority_spec.clone(),
2295 }
2296 }
2297}
2298
2299fn get_first_leaf_buffer(
2301 node: &SerializedSplitNode,
2302 path_to_buffer: &HashMap<PathBuf, BufferId>,
2303 terminal_buffers: &HashMap<usize, BufferId>,
2304 unnamed_buffers: &HashMap<String, BufferId>,
2305) -> Option<BufferId> {
2306 match node {
2307 SerializedSplitNode::Leaf {
2308 file_path,
2309 unnamed_recovery_id,
2310 ..
2311 } => file_path
2312 .as_ref()
2313 .and_then(|p| path_to_buffer.get(p).copied())
2314 .or_else(|| {
2315 unnamed_recovery_id
2316 .as_ref()
2317 .and_then(|id| unnamed_buffers.get(id).copied())
2318 }),
2319 SerializedSplitNode::Terminal { terminal_index, .. } => {
2320 terminal_buffers.get(terminal_index).copied()
2321 }
2322 SerializedSplitNode::Split { first, .. } => {
2323 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2324 }
2325 }
2326}
2327
2328fn serialize_split_node(
2333 node: &SplitNode,
2334 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2335 working_dir: &Path,
2336 terminal_buffers: &HashMap<BufferId, TerminalId>,
2337 terminal_indices: &HashMap<TerminalId, usize>,
2338 split_labels: &HashMap<SplitId, String>,
2339) -> SerializedSplitNode {
2340 serialize_split_node_pruned(
2341 node,
2342 buffer_metadata,
2343 working_dir,
2344 terminal_buffers,
2345 terminal_indices,
2346 split_labels,
2347 )
2348 .unwrap_or({
2349 SerializedSplitNode::Leaf {
2352 file_path: None,
2353 split_id: 0,
2354 label: None,
2355 unnamed_recovery_id: None,
2356 role: None,
2357 }
2358 })
2359}
2360
2361fn serialize_split_node_pruned(
2368 node: &SplitNode,
2369 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2370 working_dir: &Path,
2371 terminal_buffers: &HashMap<BufferId, TerminalId>,
2372 terminal_indices: &HashMap<TerminalId, usize>,
2373 split_labels: &HashMap<SplitId, String>,
2374) -> Option<SerializedSplitNode> {
2375 match node {
2376 SplitNode::Grouped { layout, .. } => {
2377 serialize_split_node_pruned(
2381 layout,
2382 buffer_metadata,
2383 working_dir,
2384 terminal_buffers,
2385 terminal_indices,
2386 split_labels,
2387 )
2388 }
2389 SplitNode::Leaf {
2390 buffer_id,
2391 split_id,
2392 role,
2393 } => {
2394 let raw_split_id: SplitId = (*split_id).into();
2395 let label = split_labels.get(&raw_split_id).cloned();
2396 let role = *role;
2397
2398 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2399 if let Some(index) = terminal_indices.get(terminal_id) {
2400 return Some(SerializedSplitNode::Terminal {
2401 terminal_index: *index,
2402 split_id: raw_split_id.0,
2403 label,
2404 role,
2405 });
2406 }
2407 }
2408
2409 let meta = buffer_metadata.get(buffer_id);
2410
2411 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2415 return None;
2416 }
2417
2418 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2419 if abs_path.as_os_str().is_empty() {
2420 None } else {
2422 abs_path
2423 .strip_prefix(working_dir)
2424 .ok()
2425 .map(|p| p.to_path_buf())
2426 }
2427 });
2428
2429 let unnamed_recovery_id = if file_path.is_none() {
2432 meta.and_then(|m| m.recovery_id.clone())
2433 } else {
2434 None
2435 };
2436
2437 Some(SerializedSplitNode::Leaf {
2438 file_path,
2439 split_id: raw_split_id.0,
2440 label,
2441 unnamed_recovery_id,
2442 role,
2443 })
2444 }
2445 SplitNode::Split {
2446 direction,
2447 first,
2448 second,
2449 ratio,
2450 split_id,
2451 ..
2452 } => {
2453 let raw_split_id: SplitId = (*split_id).into();
2454 let first = serialize_split_node_pruned(
2455 first,
2456 buffer_metadata,
2457 working_dir,
2458 terminal_buffers,
2459 terminal_indices,
2460 split_labels,
2461 );
2462 let second = serialize_split_node_pruned(
2463 second,
2464 buffer_metadata,
2465 working_dir,
2466 terminal_buffers,
2467 terminal_indices,
2468 split_labels,
2469 );
2470 match (first, second) {
2471 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2472 direction: match direction {
2473 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2474 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2475 },
2476 first: Box::new(f),
2477 second: Box::new(s),
2478 ratio: *ratio,
2479 split_id: raw_split_id.0,
2480 }),
2481 (Some(only), None) | (None, Some(only)) => Some(only),
2484 (None, None) => None,
2485 }
2486 }
2487 }
2488}
2489
2490fn serialize_split_view_state(
2491 view_state: &crate::view::split::SplitViewState,
2492 buffers: &HashMap<BufferId, EditorState>,
2493 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2494 working_dir: &Path,
2495 active_buffer: Option<BufferId>,
2496 terminal_buffers: &HashMap<BufferId, TerminalId>,
2497 terminal_indices: &HashMap<TerminalId, usize>,
2498) -> SerializedSplitViewState {
2499 let mut open_tabs = Vec::new();
2500 let mut open_files = Vec::new();
2501 let mut active_tab_index = None;
2502
2503 for buffer_id in view_state.buffer_tab_ids() {
2505 let buffer_id = &buffer_id;
2506 let tab_index = open_tabs.len();
2507 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2508 if let Some(idx) = terminal_indices.get(terminal_id) {
2509 open_tabs.push(SerializedTabRef::Terminal(*idx));
2510 if Some(*buffer_id) == active_buffer {
2511 active_tab_index = Some(tab_index);
2512 }
2513 continue;
2514 }
2515 }
2516
2517 if let Some(meta) = buffer_metadata.get(buffer_id) {
2518 if let Some(abs_path) = meta.file_path() {
2519 if abs_path.as_os_str().is_empty() {
2520 if let Some(ref recovery_id) = meta.recovery_id {
2522 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2523 if Some(*buffer_id) == active_buffer {
2524 active_tab_index = Some(tab_index);
2525 }
2526 }
2527 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2528 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2529 open_files.push(rel_path.to_path_buf());
2530 if Some(*buffer_id) == active_buffer {
2531 active_tab_index = Some(tab_index);
2532 }
2533 } else {
2534 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2536 if Some(*buffer_id) == active_buffer {
2537 active_tab_index = Some(tab_index);
2538 }
2539 }
2540 }
2541 }
2542 }
2543
2544 let active_file_index = active_tab_index
2546 .and_then(|idx| open_tabs.get(idx))
2547 .and_then(|tab| match tab {
2548 SerializedTabRef::File(path) => {
2549 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2550 }
2551 _ => None,
2552 })
2553 .unwrap_or(0);
2554
2555 let mut file_states = HashMap::new();
2557 for (buffer_id, buf_state) in &view_state.keyed_states {
2558 let Some(meta) = buffer_metadata.get(buffer_id) else {
2559 continue;
2560 };
2561 let Some(abs_path) = meta.file_path() else {
2562 continue;
2563 };
2564
2565 let state_key = if abs_path.as_os_str().is_empty() {
2567 if let Some(ref recovery_id) = meta.recovery_id {
2569 PathBuf::from(format!("__unnamed__{}", recovery_id))
2570 } else {
2571 continue;
2572 }
2573 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2574 rp.to_path_buf()
2575 } else {
2576 abs_path.to_path_buf()
2578 };
2579
2580 let primary_cursor = buf_state.cursors.primary();
2581 let folds = buffers
2582 .get(buffer_id)
2583 .map(|state| {
2584 buf_state
2585 .folds
2586 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2587 .into_iter()
2588 .map(|range| SerializedFoldRange {
2589 header_line: range.header_line,
2590 end_line: range.end_line,
2591 placeholder: range.placeholder,
2592 header_text: range.header_text,
2593 })
2594 .collect::<Vec<_>>()
2595 })
2596 .unwrap_or_default();
2597
2598 file_states.insert(
2599 state_key,
2600 SerializedFileState {
2601 cursor: SerializedCursor {
2602 position: primary_cursor.position,
2603 anchor: primary_cursor.anchor,
2604 sticky_column: primary_cursor.sticky_column,
2605 },
2606 additional_cursors: buf_state
2607 .cursors
2608 .iter()
2609 .skip(1) .map(|(_, cursor)| SerializedCursor {
2611 position: cursor.position,
2612 anchor: cursor.anchor,
2613 sticky_column: cursor.sticky_column,
2614 })
2615 .collect(),
2616 scroll: SerializedScroll {
2617 top_byte: buf_state.viewport.top_byte,
2618 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2619 left_column: buf_state.viewport.left_column,
2620 },
2621 view_mode: match buf_state.view_mode {
2622 ViewMode::Source => SerializedViewMode::Source,
2623 ViewMode::PageView => SerializedViewMode::PageView,
2624 },
2625 compose_width: buf_state.compose_width,
2626 plugin_state: buf_state.plugin_state.clone(),
2627 folds,
2628 },
2629 );
2630 }
2631
2632 let active_view_mode = active_buffer
2634 .and_then(|id| view_state.keyed_states.get(&id))
2635 .map(|bs| match bs.view_mode {
2636 ViewMode::Source => SerializedViewMode::Source,
2637 ViewMode::PageView => SerializedViewMode::PageView,
2638 })
2639 .unwrap_or(SerializedViewMode::Source);
2640 let active_compose_width = active_buffer
2641 .and_then(|id| view_state.keyed_states.get(&id))
2642 .and_then(|bs| bs.compose_width);
2643
2644 SerializedSplitViewState {
2645 open_tabs,
2646 active_tab_index,
2647 open_files,
2648 active_file_index,
2649 file_states,
2650 tab_scroll_offset: view_state.tab_scroll_offset,
2651 view_mode: active_view_mode,
2652 compose_width: active_compose_width,
2653 }
2654}
2655
2656fn serialize_bookmarks(
2657 bookmarks: &BookmarkState,
2658 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2659 working_dir: &Path,
2660) -> HashMap<char, SerializedBookmark> {
2661 bookmarks
2662 .iter()
2663 .filter_map(|(key, bookmark)| {
2664 buffer_metadata
2665 .get(&bookmark.buffer_id)
2666 .and_then(|meta| meta.file_path())
2667 .and_then(|abs_path| {
2668 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2669 (
2670 key,
2671 SerializedBookmark {
2672 file_path: rel_path.to_path_buf(),
2673 position: bookmark.position,
2674 },
2675 )
2676 })
2677 })
2678 })
2679 .collect()
2680}
2681
2682fn collect_file_paths_from_states(
2684 split_states: &HashMap<usize, SerializedSplitViewState>,
2685) -> Vec<PathBuf> {
2686 let mut paths = Vec::new();
2687 for state in split_states.values() {
2688 if !state.open_tabs.is_empty() {
2689 for tab in &state.open_tabs {
2690 if let SerializedTabRef::File(path) = tab {
2691 if !paths.contains(path) {
2692 paths.push(path.clone());
2693 }
2694 }
2695 }
2696 } else {
2697 for path in &state.open_files {
2698 if !paths.contains(path) {
2699 paths.push(path.clone());
2700 }
2701 }
2702 }
2703 }
2704 paths
2705}
2706
2707fn get_expanded_dirs(
2709 explorer: &crate::view::file_tree::FileTreeView,
2710 working_dir: &Path,
2711) -> Vec<PathBuf> {
2712 let mut expanded = Vec::new();
2713 let tree = explorer.tree();
2714
2715 for node in tree.all_nodes() {
2717 if node.is_expanded() && node.is_dir() {
2718 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2720 expanded.push(rel_path.to_path_buf());
2721 }
2722 }
2723 }
2724
2725 expanded
2726}