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 restore_window(
200 &mut self,
201 id: fresh_core::WindowId,
202 opened_files: bool,
203 ) -> Result<bool, WorkspaceError> {
204 let restored = self.restore_workspace_for(id)?;
205 if let Some(win) = self.windows.get_mut(&id) {
206 win.apply_fresh_session_explorer_default(opened_files, restored);
207 }
208 Ok(restored)
209 }
210
211 pub fn restore_active_window_on_launch(
216 &mut self,
217 opened_files: bool,
218 ) -> Result<bool, WorkspaceError> {
219 self.restore_window(self.active_window, opened_files)
220 }
221
222 pub fn apply_active_window_explorer_default(
228 &mut self,
229 opened_files: bool,
230 workspace_restored: bool,
231 ) {
232 self.active_window_mut()
233 .apply_fresh_session_explorer_default(opened_files, workspace_restored);
234 }
235
236 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
242 if !self.config.editor.hot_exit {
243 return Ok(0);
244 }
245
246 let entries = self.recovery_service.lock().unwrap().list_recoverable()?;
247 if entries.is_empty() {
248 return Ok(0);
249 }
250
251 let buffer_files: Vec<_> = self
253 .buffers()
254 .iter()
255 .filter_map(|(buffer_id, state)| {
256 let path = state.buffer.file_path()?.to_path_buf();
257 if path.as_os_str().is_empty() {
258 return None; }
260 Some((*buffer_id, path))
261 })
262 .collect();
263
264 let mut recovered = 0;
265 for (buffer_id, file_path) in buffer_files {
266 let recovery_id = self
267 .recovery_service
268 .lock()
269 .unwrap()
270 .get_buffer_id(Some(&file_path));
271 let entry = entries.iter().find(|e| e.id == recovery_id);
272 if let Some(entry) = entry {
273 let loaded = self.recovery_service.lock().unwrap().load_recovery(entry);
274 match loaded {
275 Ok(crate::services::recovery::RecoveryResult::Recovered {
276 content, ..
277 }) => {
278 let mut mutated = false;
279 if let Some(state) = self
280 .windows
281 .get_mut(&self.active_window)
282 .map(|w| &mut w.buffers)
283 .expect("active window present")
284 .get_mut(&buffer_id)
285 {
286 let current_len = state.buffer.total_bytes();
287 let text = String::from_utf8_lossy(&content).into_owned();
288 let current = state.buffer.get_text_range_mut(0, current_len).ok();
289 let current_text = current
290 .as_ref()
291 .map(|b| String::from_utf8_lossy(b).into_owned());
292 if current_text.as_deref() != Some(&text) {
293 state.buffer.delete(0..current_len);
294 state.buffer.insert(0, &text);
295 state.buffer.set_modified(true);
296 state.buffer.set_recovery_pending(false);
297 if let Some(log) =
300 self.active_window_mut().event_logs.get_mut(&buffer_id)
301 {
302 log.clear_saved_position();
303 }
304 mutated = true;
305 recovered += 1;
306 tracing::info!(
307 "Restored unsaved changes for {:?} from hot exit recovery",
308 file_path
309 );
310 }
311 }
312 if mutated {
313 self.sync_lsp_after_recovery_replay(buffer_id);
314 }
315 }
316 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
317 chunks,
318 ..
319 }) => {
320 let mut mutated = false;
321 if let Some(state) = self
322 .windows
323 .get_mut(&self.active_window)
324 .map(|w| &mut w.buffers)
325 .expect("active window present")
326 .get_mut(&buffer_id)
327 {
328 for chunk in chunks.into_iter().rev() {
329 let text = String::from_utf8_lossy(&chunk.content).into_owned();
330 if chunk.original_len > 0 {
331 state
332 .buffer
333 .delete(chunk.offset..chunk.offset + chunk.original_len);
334 }
335 state.buffer.insert(chunk.offset, &text);
336 }
337 state.buffer.set_modified(true);
338 state.buffer.set_recovery_pending(false);
339 if let Some(log) =
342 self.active_window_mut().event_logs.get_mut(&buffer_id)
343 {
344 log.clear_saved_position();
345 }
346 mutated = true;
347 recovered += 1;
348 tracing::info!(
349 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
350 file_path
351 );
352 }
353 if mutated {
354 self.sync_lsp_after_recovery_replay(buffer_id);
355 }
356 }
357 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
358 original_path,
359 ..
360 }) => {
361 let name = original_path
362 .file_name()
363 .unwrap_or_default()
364 .to_string_lossy();
365 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
366 self.set_status_message(format!(
367 "{} changed on disk; unsaved changes not restored",
368 name
369 ));
370 }
371 Ok(_) => {} Err(e) => {
373 tracing::debug!(
374 "Failed to load hot exit recovery for {:?}: {}",
375 file_path,
376 e
377 );
378 }
379 }
380 }
381 }
382
383 Ok(recovered)
384 }
385
386 fn restore_config_overrides(&mut self, overrides: &WorkspaceConfigOverrides) {
391 if let Some(line_numbers) = overrides.line_numbers {
392 self.config_mut().editor.line_numbers = line_numbers;
393 }
394 if let Some(relative_line_numbers) = overrides.relative_line_numbers {
395 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
396 }
397 if let Some(line_wrap) = overrides.line_wrap {
398 self.config_mut().editor.line_wrap = line_wrap;
399 }
400 if let Some(syntax_highlighting) = overrides.syntax_highlighting {
401 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
402 }
403 if let Some(enable_inlay_hints) = overrides.enable_inlay_hints {
404 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
405 }
406 }
411
412 pub fn save_workspace_for(&mut self, id: fresh_core::WindowId) -> Result<(), WorkspaceError> {
417 let Some(win) = self.windows.get(&id) else {
418 return Ok(());
419 };
420
421 win.sync_terminal_backing_files();
424 win.save_all_global_file_states();
425
426 let workspace = win.capture_workspace();
427
428 if workspace.has_no_real_content() && win.has_any_virtual_buffer() {
433 let root = win.root.clone();
434 let on_disk = if let Some(ref session_name) = self.session_name {
435 Workspace::load_session(session_name, &root).ok().flatten()
436 } else {
437 Workspace::load(&root).ok().flatten()
438 };
439 if let Some(existing) = on_disk {
440 if !existing.has_no_preservable_content() {
441 tracing::info!(
442 "Skipping workspace save: only virtual buffers are open, \
443 on-disk workspace already has preservable file content"
444 );
445 return Ok(());
446 }
447 }
448 }
449
450 if let Some(ref session_name) = self.session_name {
452 workspace.save_session(session_name)
453 } else {
454 workspace.save()
455 }
456 }
457
458 pub fn restore_workspace_for(
470 &mut self,
471 id: fresh_core::WindowId,
472 ) -> Result<bool, WorkspaceError> {
473 let Some(root) = self.windows.get(&id).map(|w| w.root.clone()) else {
474 return Ok(false);
475 };
476
477 let workspace = if let Some(ref session_name) = self.session_name {
478 Workspace::load_session(session_name, &root)?
479 } else {
480 Workspace::load(&root)?
481 };
482 let Some(workspace) = workspace else {
483 tracing::debug!("No workspace found for {:?}", root);
484 return Ok(false);
485 };
486 tracing::info!("Found workspace for {:?}, applying...", root);
487
488 self.restore_config_overrides(&workspace.config_overrides);
490 let populated = self
497 .windows
498 .get(&id)
499 .map(|w| w.buffers.splits().is_some() && w.buffers.len() > 0)
500 .unwrap_or(false);
501
502 let session = self.session_name.clone();
503 if populated {
504 let win = self
507 .windows
508 .get_mut(&id)
509 .expect("window present for restore");
510 win.apply_workspace_layout(&workspace, session.as_deref());
511 win.authority_spec = workspace.authority_spec.clone();
515 } else {
516 let old = self
522 .windows
523 .remove(&id)
524 .expect("window present for restore");
525 let (label, root2, authority, resources, tw, th, pstate) = (
526 old.label,
527 old.root,
528 old.authority,
529 old.resources,
530 old.terminal_width,
531 old.terminal_height,
532 old.plugin_state,
533 );
534 let mut built = crate::app::window::Window::from_workspace(
535 id, label, root2, authority, resources, &workspace,
536 );
537 built.terminal_width = tw;
538 built.terminal_height = th;
539 built.plugin_state = pstate;
540 built.authority_spec = workspace.authority_spec.clone();
541 self.windows.insert(id, built);
542 }
543
544 if id == self.active_window {
548 #[cfg(feature = "plugins")]
549 {
550 let buffer_id = self.active_buffer();
551 self.update_plugin_state_snapshot();
552 tracing::debug!(
553 "Firing buffer_activated for active buffer {:?} after workspace restore",
554 buffer_id
555 );
556 self.plugin_manager.read().unwrap().run_hook(
557 "buffer_activated",
558 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
559 );
560 }
561 }
562
563 Ok(true)
564 }
565
566 pub fn save_all_windows_workspaces(&mut self) -> Result<(), WorkspaceError> {
573 let targets: Vec<fresh_core::WindowId> = self
574 .windows
575 .iter()
576 .filter(|(id, w)| {
581 w.buffers.splits().is_some() && !self.materialize_pending.contains(id)
582 })
583 .map(|(id, _)| *id)
584 .collect();
585
586 let mut first_err = None;
587 for id in targets {
588 if let Err(e) = self.save_workspace_for(id) {
589 tracing::warn!("Failed to save workspace for window {id}: {e}");
590 if first_err.is_none() {
591 first_err = Some(e);
592 }
593 }
594 }
595
596 match first_err {
597 Some(e) => Err(e),
598 None => Ok(()),
599 }
600 }
601
602 pub(crate) fn materialize_window(&mut self, id: fresh_core::WindowId) {
613 if !self.materialize_pending.remove(&id) {
614 return;
615 }
616 let saved_plugin_state = self.plugin_global_state.clone();
617 match self.restore_window(id, false) {
622 Ok(true) => tracing::debug!("Materialized window {id} from workspace"),
623 Ok(false) => {
624 tracing::trace!("No persisted workspace for window {id}; empty seed kept")
625 }
626 Err(e) => tracing::warn!("Failed to materialize window {id}: {e}"),
627 }
628 self.plugin_global_state = saved_plugin_state;
629 }
630
631 pub fn materialize_all_windows(&mut self) {
638 let pending: Vec<fresh_core::WindowId> = self.materialize_pending.iter().copied().collect();
639 for id in pending {
640 self.materialize_window(id);
641 }
642 }
643}
644
645impl crate::app::window::Window {
646 fn restore_terminals_from_workspace(
647 &mut self,
648 terminals: &[SerializedTerminalWorkspace],
649 ) -> HashMap<usize, BufferId> {
650 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
651 if terminals.is_empty() {
652 return terminal_buffer_map;
653 }
654 let __window_bridge = self.bridge.clone();
655 self.terminal_manager.set_async_bridge(__window_bridge);
656 for terminal in terminals {
657 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
658 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
659 self.set_terminal_interaction_mode(
668 buffer_id,
669 crate::app::window::TerminalInteractionMode::Live,
670 );
671 }
672 }
673 terminal_buffer_map
674 }
675
676 fn restore_bookmarks_from_workspace(
678 &mut self,
679 bookmarks: &HashMap<char, SerializedBookmark>,
680 path_to_buffer: &HashMap<PathBuf, BufferId>,
681 ) {
682 for (key, bookmark) in bookmarks {
683 let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) else {
684 continue;
685 };
686 if let Some(buffer) = self.buffers.get(&buffer_id) {
687 let pos = bookmark.position.min(buffer.buffer.len());
688 self.bookmarks.set(
689 *key,
690 Bookmark {
691 buffer_id,
692 position: pos,
693 },
694 );
695 }
696 }
697 }
698
699 fn clean_orphaned_buffers(&mut self) {
702 let referenced: HashSet<BufferId> = self
703 .buffers
704 .splits()
705 .map(|(_, vs)| vs)
706 .expect("active window must have a populated split layout")
707 .values()
708 .flat_map(|vs| vs.buffer_tab_ids())
709 .collect();
710 let orphans: Vec<BufferId> = self
711 .buffers
712 .iter()
713 .filter(|(id, state)| {
714 !referenced.contains(id)
715 && state.buffer.file_path().is_none()
716 && !state.buffer.is_modified()
717 })
718 .map(|(id, _)| *id)
719 .collect();
720 for id in orphans {
721 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
722 self.buffers.remove(&id);
723 self.event_logs.remove(&id);
724 self.buffer_metadata.remove(&id);
725 }
726 }
727
728 fn log_restore_summary(&mut self, session_name: Option<&str>) {
731 tracing::debug!(
732 "Workspace restore complete: {} splits, {} buffers",
733 self.buffers
734 .splits()
735 .map(|(_, vs)| vs)
736 .expect("active window must have a populated split layout")
737 .len(),
738 self.buffers.len()
739 );
740 let restored_count = self.buffers.count_where(|id, _| {
741 self.buffer_metadata
742 .get(&id)
743 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
744 });
745 if restored_count == 0 {
746 return;
747 }
748 let msg = match session_name.map(|n| format!("session '{}'", n)) {
749 Some(label) => format!("Restored {} ({} buffer(s))", label, restored_count),
750 None => format!(
751 "Restored {} buffer(s) from previous session",
752 restored_count
753 ),
754 };
755 self.set_status_message(msg);
756 }
757
758 fn restore_terminal_from_workspace(
767 &mut self,
768 terminal: &SerializedTerminalWorkspace,
769 ) -> Option<BufferId> {
770 let terminals_root = self
772 .resources
773 .dir_context
774 .terminal_dir_for(self.root.as_path());
775 let log_path = if terminal.log_path.is_absolute() {
776 terminal.log_path.clone()
777 } else {
778 terminals_root.join(&terminal.log_path)
779 };
780 let backing_path = if terminal.backing_path.is_absolute() {
781 terminal.backing_path.clone()
782 } else {
783 terminals_root.join(&terminal.backing_path)
784 };
785
786 #[allow(clippy::let_underscore_must_use)]
788 let _ = crate::app::terminal::terminal_backing_fs().create_dir_all(
789 log_path
790 .parent()
791 .or_else(|| backing_path.parent())
792 .unwrap_or(&terminals_root),
793 );
794
795 let predicted_id = self.terminal_manager.next_terminal_id();
797 self.terminal_log_files
798 .insert(predicted_id, log_path.clone());
799 self.terminal_backing_files
800 .insert(predicted_id, backing_path.clone());
801
802 let resume_argv = terminal
811 .agent_resume
812 .as_ref()
813 .map(|r| &r.argv)
814 .filter(|argv| !argv.is_empty() && self.resources.config.terminal.resume_agents);
815 let spawn_argv =
816 resume_argv.or_else(|| terminal.command.as_ref().filter(|argv| !argv.is_empty()));
817 let wrapper_for_spawn = match spawn_argv {
824 Some(argv) => self.authority().terminal_command(argv),
825 None => self.resolved_terminal_wrapper(),
826 };
827 let wrapper_for_spawn = self.apply_remote_terminal_env(wrapper_for_spawn);
828 let env_delta = self.terminal_env_delta(&wrapper_for_spawn);
829 let terminal_id = match self.terminal_manager.spawn(
830 terminal.cols,
831 terminal.rows,
832 terminal.cwd.clone(),
833 Some(log_path.clone()),
834 Some(backing_path.clone()),
835 wrapper_for_spawn,
836 env_delta,
837 ) {
838 Ok(id) => id,
839 Err(e) => {
840 tracing::warn!(
841 "Failed to restore terminal {}: {}",
842 terminal.terminal_index,
843 e
844 );
845 return None;
846 }
847 };
848
849 if terminal_id != predicted_id {
851 self.terminal_log_files
852 .insert(terminal_id, log_path.clone());
853 self.terminal_backing_files
854 .insert(terminal_id, backing_path.clone());
855 self.terminal_log_files.remove(&predicted_id);
856 self.terminal_backing_files.remove(&predicted_id);
857 }
858
859 if let Some(argv) = terminal.command.as_ref() {
863 self.terminal_commands.insert(terminal_id, argv.clone());
864 }
865 if let Some(resume) = terminal.agent_resume.as_ref() {
866 if !resume.argv.is_empty() {
867 self.terminal_resume_commands
868 .insert(terminal_id, resume.argv.clone());
869 }
870 }
871
872 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
874
875 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
878
879 Some(buffer_id)
880 }
881
882 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
887 if !backing_path.exists() {
889 return;
890 }
891
892 let large_file_threshold = self.resources.config.editor.large_file_threshold_bytes as usize;
893 if let Ok(new_state) = EditorState::from_file_with_languages(
894 backing_path,
895 self.terminal_width,
896 self.terminal_height,
897 large_file_threshold,
898 &self.resources.grammar_registry,
899 &self.resources.config.languages,
900 crate::app::terminal::terminal_backing_fs(),
901 ) {
902 self.install_terminal_buffer_state(buffer_id, new_state);
903 }
904 }
905
906 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
908 for (buffer_id, metadata) in &self.buffer_metadata {
910 if let Some(file_path) = metadata.file_path() {
911 if file_path == path {
912 return Ok(*buffer_id);
913 }
914 }
915 }
916
917 self.open_file_no_focus(path).map_err(WorkspaceError::Io)
919 }
920
921 #[allow(clippy::too_many_arguments)]
923 fn restore_split_node(
924 &mut self,
925 node: &SerializedSplitNode,
926 path_to_buffer: &HashMap<PathBuf, BufferId>,
927 terminal_buffers: &HashMap<usize, BufferId>,
928 unnamed_buffers: &HashMap<String, BufferId>,
929 split_states: &HashMap<usize, SerializedSplitViewState>,
930 split_id_map: &mut HashMap<usize, SplitId>,
931 is_first_leaf: bool,
932 ) {
933 match node {
934 SerializedSplitNode::Leaf {
935 file_path,
936 split_id,
937 label,
938 unnamed_recovery_id,
939 role,
940 } => {
941 let buffer_id = file_path
943 .as_ref()
944 .and_then(|p| path_to_buffer.get(p).copied())
945 .or_else(|| {
946 unnamed_recovery_id
947 .as_ref()
948 .and_then(|id| unnamed_buffers.get(id).copied())
949 })
950 .unwrap_or(self.active_buffer());
951
952 let current_leaf_id = if is_first_leaf {
953 let leaf_id = self
955 .buffers
956 .splits()
957 .map(|(mgr, _)| mgr)
958 .expect("active window must have a populated split layout")
959 .active_split();
960 self.set_pane_buffer(leaf_id, buffer_id);
961 leaf_id
962 } else {
963 self.buffers
965 .splits()
966 .map(|(mgr, _)| mgr)
967 .expect("active window must have a populated split layout")
968 .active_split()
969 };
970
971 split_id_map.insert(*split_id, current_leaf_id.into());
973
974 if let Some(label) = label {
976 self.buffers
977 .split_manager_mut()
978 .expect("active window must have a populated split layout")
979 .set_label(current_leaf_id, label.clone());
980 }
981
982 if let Some(role) = role {
985 self.buffers
986 .split_manager_mut()
987 .expect("active window must have a populated split layout")
988 .clear_role(*role);
989 self.buffers
990 .split_manager_mut()
991 .expect("active window must have a populated split layout")
992 .set_leaf_role(current_leaf_id, Some(*role));
993 }
994
995 self.restore_split_view_state(
997 current_leaf_id,
998 *split_id,
999 split_states,
1000 path_to_buffer,
1001 terminal_buffers,
1002 unnamed_buffers,
1003 );
1004 }
1005 SerializedSplitNode::Terminal {
1006 terminal_index,
1007 split_id,
1008 label,
1009 role,
1010 } => {
1011 let buffer_id = terminal_buffers
1012 .get(terminal_index)
1013 .copied()
1014 .unwrap_or(self.active_buffer());
1015
1016 let current_leaf_id = if is_first_leaf {
1017 let leaf_id = self
1018 .buffers
1019 .splits()
1020 .map(|(mgr, _)| mgr)
1021 .expect("active window must have a populated split layout")
1022 .active_split();
1023 self.set_pane_buffer(leaf_id, buffer_id);
1024 leaf_id
1025 } else {
1026 self.buffers
1027 .splits()
1028 .map(|(mgr, _)| mgr)
1029 .expect("active window must have a populated split layout")
1030 .active_split()
1031 };
1032
1033 split_id_map.insert(*split_id, current_leaf_id.into());
1034
1035 if let Some(label) = label {
1037 self.buffers
1038 .split_manager_mut()
1039 .expect("active window must have a populated split layout")
1040 .set_label(current_leaf_id, label.clone());
1041 }
1042
1043 if let Some(role) = role {
1046 self.buffers
1047 .split_manager_mut()
1048 .expect("active window must have a populated split layout")
1049 .clear_role(*role);
1050 self.buffers
1051 .split_manager_mut()
1052 .expect("active window must have a populated split layout")
1053 .set_leaf_role(current_leaf_id, Some(*role));
1054 }
1055
1056 self.buffers
1057 .split_manager_mut()
1058 .expect("active window must have a populated split layout")
1059 .set_split_buffer(current_leaf_id, buffer_id);
1060
1061 self.restore_split_view_state(
1062 current_leaf_id,
1063 *split_id,
1064 split_states,
1065 path_to_buffer,
1066 terminal_buffers,
1067 unnamed_buffers,
1068 );
1069 }
1070 SerializedSplitNode::Split {
1071 direction,
1072 first,
1073 second,
1074 ratio,
1075 split_id,
1076 } => {
1077 self.restore_split_node(
1079 first,
1080 path_to_buffer,
1081 terminal_buffers,
1082 unnamed_buffers,
1083 split_states,
1084 split_id_map,
1085 is_first_leaf,
1086 );
1087
1088 let second_buffer_id = get_first_leaf_buffer(
1090 second,
1091 path_to_buffer,
1092 terminal_buffers,
1093 unnamed_buffers,
1094 )
1095 .unwrap_or(self.active_buffer());
1096
1097 let split_direction = match direction {
1099 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1100 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1101 };
1102
1103 match self
1105 .buffers
1106 .split_manager_mut()
1107 .expect("active window must have a populated split layout")
1108 .split_active(split_direction, second_buffer_id, *ratio)
1109 {
1110 Ok(new_leaf_id) => {
1111 let mut view_state = SplitViewState::with_buffer(
1113 self.terminal_width,
1114 self.terminal_height,
1115 second_buffer_id,
1116 );
1117 view_state.apply_config_defaults(
1118 self.resources.config.editor.line_numbers,
1119 self.resources.config.editor.highlight_current_line,
1120 self.resolve_line_wrap_for_buffer(second_buffer_id),
1121 self.resources.config.editor.wrap_indent,
1122 self.resolve_wrap_column_for_buffer(second_buffer_id),
1123 self.resources.config.editor.rulers.clone(),
1124 self.resources.config.editor.scroll_offset,
1125 );
1126 self.buffers
1127 .split_view_states_mut()
1128 .expect("active window must have a populated split layout")
1129 .insert(new_leaf_id, view_state);
1130
1131 split_id_map.insert(*split_id, new_leaf_id.into());
1133
1134 self.restore_split_node(
1136 second,
1137 path_to_buffer,
1138 terminal_buffers,
1139 unnamed_buffers,
1140 split_states,
1141 split_id_map,
1142 false,
1143 );
1144 }
1145 Err(e) => {
1146 tracing::error!("Failed to create split during workspace restore: {}", e);
1147 }
1148 }
1149 }
1150 }
1151 }
1152
1153 fn restore_split_view_state(
1155 &mut self,
1156 current_split_id: LeafId,
1157 saved_split_id: usize,
1158 split_states: &HashMap<usize, SerializedSplitViewState>,
1159 path_to_buffer: &HashMap<PathBuf, BufferId>,
1160 terminal_buffers: &HashMap<usize, BufferId>,
1161 unnamed_buffers: &HashMap<String, BufferId>,
1162 ) {
1163 let Some(split_state) = split_states.get(&saved_split_id) else {
1165 return;
1166 };
1167
1168 let split_buf_for_current = self
1172 .buffers
1173 .split_manager()
1174 .expect("active window must have a populated split layout")
1175 .buffer_for_split(current_split_id);
1176 let active_buffer_id = self
1177 .buffers
1178 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1179 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1180 return None;
1181 };
1182 let mut active_buffer_id: Option<BufferId> = None;
1183 if !split_state.open_tabs.is_empty() {
1184 view_state.open_buffers.clear();
1187
1188 for tab in &split_state.open_tabs {
1189 match tab {
1190 SerializedTabRef::File(rel_path) => {
1191 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1192 if !view_state.has_buffer(buffer_id) {
1193 view_state.add_buffer(buffer_id);
1194 }
1195 view_state.ensure_buffer_state(buffer_id);
1197 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1198 let buf_state =
1199 view_state.buffer_state_mut(buffer_id).unwrap();
1200 buf_state.viewport.line_wrap_enabled = false;
1201 buf_state.show_line_numbers = false;
1205 buf_state.highlight_current_line = false;
1206 }
1207 }
1208 }
1209 SerializedTabRef::Terminal(index) => {
1210 if let Some(&buffer_id) = terminal_buffers.get(index) {
1211 if !view_state.has_buffer(buffer_id) {
1212 view_state.add_buffer(buffer_id);
1213 }
1214 let buf_state = view_state.ensure_buffer_state(buffer_id);
1215 buf_state.viewport.line_wrap_enabled = false;
1216 buf_state.show_line_numbers = false;
1220 buf_state.highlight_current_line = false;
1221 }
1222 }
1223 SerializedTabRef::Unnamed(recovery_id) => {
1224 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1225 if !view_state.has_buffer(buffer_id) {
1226 view_state.add_buffer(buffer_id);
1227 }
1228 view_state.ensure_buffer_state(buffer_id);
1229 }
1230 }
1231 }
1232 }
1233
1234 if view_state.open_buffers.is_empty() {
1239 if let Some(buf) = split_buf_for_current {
1240 view_state.add_buffer(buf);
1241 view_state.ensure_buffer_state(buf);
1242 }
1243 }
1244
1245 if let Some(active_idx) = split_state.active_tab_index {
1246 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1247 active_buffer_id = match tab {
1248 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1249 SerializedTabRef::Terminal(index) => {
1250 terminal_buffers.get(index).copied()
1251 }
1252 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1253 };
1254 }
1255 }
1256 } else {
1257 for rel_path in &split_state.open_files {
1259 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1260 if !view_state.has_buffer(buffer_id) {
1261 view_state.add_buffer(buffer_id);
1262 }
1263 view_state.ensure_buffer_state(buffer_id);
1264 }
1265 }
1266
1267 let active_file_path =
1268 split_state.open_files.get(split_state.active_file_index);
1269 active_buffer_id =
1270 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1271 }
1272
1273 for (rel_path, file_state) in &split_state.file_states {
1275 let rel_str = rel_path.to_string_lossy();
1277 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1278 match unnamed_buffers.get(recovery_id).copied() {
1279 Some(id) => id,
1280 None => continue,
1281 }
1282 } else {
1283 match path_to_buffer.get(rel_path).copied() {
1284 Some(id) => id,
1285 None => continue,
1286 }
1287 };
1288 let max_pos = __buffers_mut
1289 .get(&buffer_id)
1290 .map(|b| b.buffer.len())
1291 .unwrap_or(0);
1292
1293 let buf_state = view_state.ensure_buffer_state(buffer_id);
1295
1296 let cursor_pos = file_state.cursor.position.min(max_pos);
1297 buf_state.cursors.primary_mut().position = cursor_pos;
1298 buf_state.cursors.primary_mut().anchor =
1299 file_state.cursor.anchor.map(|a| a.min(max_pos));
1300 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1301
1302 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1303 buf_state.viewport.top_view_line_offset =
1304 file_state.scroll.top_view_line_offset;
1305 buf_state.viewport.left_column = file_state.scroll.left_column;
1306 buf_state.viewport.set_skip_resize_sync();
1307
1308 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1316 super::navigation::reconcile_restored_buffer_view(
1317 buf_state,
1318 &mut state.buffer,
1319 );
1320
1321 let line = state
1329 .buffer
1330 .offset_to_position(cursor_pos)
1331 .map(|p| p.line)
1332 .unwrap_or(0);
1333 state.primary_cursor_line_number =
1334 crate::model::buffer::LineNumber::Absolute(line);
1335 }
1336
1337 buf_state.view_mode = match file_state.view_mode {
1339 SerializedViewMode::Source => ViewMode::Source,
1340 SerializedViewMode::PageView => ViewMode::PageView,
1341 };
1342 buf_state.compose_width = file_state.compose_width;
1343 if let Some(line_numbers) = file_state.line_numbers {
1347 buf_state.line_numbers_override = Some(line_numbers);
1348 buf_state.show_line_numbers = line_numbers;
1349 }
1350 if let Some(line_wrap) = file_state.line_wrap {
1351 buf_state.line_wrap_override = Some(line_wrap);
1352 buf_state.viewport.line_wrap_enabled = line_wrap;
1353 }
1354 buf_state.plugin_state = file_state.plugin_state.clone();
1355 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1356 buf_state.folds.clear(&mut state.marker_list);
1357 for fold in &file_state.folds {
1358 let Some(resolved_header) = resolve_fold_header_line(
1365 &state.buffer,
1366 fold.header_line,
1367 fold.header_text.as_deref(),
1368 ) else {
1369 tracing::debug!(
1370 "Dropping stale fold: header_line={} no longer matches stored \
1371 header_text after external edit",
1372 fold.header_line,
1373 );
1374 continue;
1375 };
1376
1377 let shift = resolved_header as i64 - fold.header_line as i64;
1379 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1380 let start_line = resolved_header.saturating_add(1);
1381 let end_line = adjusted_end;
1382 if start_line > end_line {
1383 continue;
1384 }
1385 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1386 else {
1387 continue;
1388 };
1389 let end_byte = state
1390 .buffer
1391 .line_start_offset(end_line.saturating_add(1))
1392 .unwrap_or_else(|| state.buffer.len());
1393 buf_state.folds.add(
1394 &mut state.marker_list,
1395 start_byte,
1396 end_byte,
1397 fold.placeholder.clone(),
1398 );
1399 }
1400 }
1401
1402 tracing::trace!(
1403 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1404 rel_path,
1405 cursor_pos,
1406 buf_state.viewport.top_byte,
1407 buf_state.view_mode,
1408 );
1409 }
1410
1411 if active_buffer_id.is_none() {
1428 active_buffer_id = view_state.buffer_tab_ids().next();
1429 }
1430
1431 let restored_view_mode = match split_state.view_mode {
1434 SerializedViewMode::Source => ViewMode::Source,
1435 SerializedViewMode::PageView => ViewMode::PageView,
1436 };
1437
1438 if let Some(active_buf_id) = active_buffer_id {
1439 view_state.switch_buffer(active_buf_id);
1441
1442 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
1444 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
1445 });
1446 if !active_has_file_state {
1447 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1448 view_state.active_state_mut().compose_width = split_state.compose_width;
1449 }
1450
1451 }
1453 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1454 active_buffer_id
1455 })
1456 .flatten();
1457
1458 if let Some(active_buf_id) = active_buffer_id {
1462 self.buffers
1463 .split_manager_mut()
1464 .expect("active window must have a populated split layout")
1465 .set_split_buffer(current_split_id, active_buf_id);
1466 }
1467 }
1468
1469 fn restore_search_options(&mut self, opts: &SearchOptions) {
1470 self.search_case_sensitive = opts.case_sensitive;
1471 self.search_whole_word = opts.whole_word;
1472 self.search_use_regex = opts.use_regex;
1473 self.search_confirm_each = opts.confirm_each;
1474 }
1475
1476 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
1477 tracing::debug!(
1478 "Restoring histories: {} search, {} replace, {} goto_line",
1479 histories.search.len(),
1480 histories.replace.len(),
1481 histories.goto_line.len()
1482 );
1483 for item in &histories.search {
1484 self.prompt_histories
1485 .entry("search".to_string())
1486 .or_default()
1487 .push(item.clone());
1488 }
1489 for item in &histories.replace {
1490 self.prompt_histories
1491 .entry("replace".to_string())
1492 .or_default()
1493 .push(item.clone());
1494 }
1495 for item in &histories.goto_line {
1496 self.prompt_histories
1497 .entry("goto_line".to_string())
1498 .or_default()
1499 .push(item.clone());
1500 }
1501 }
1502
1503 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
1504 self.file_explorer_visible = fe.visible;
1505 self.file_explorer_width = fe.width;
1506 self.file_explorer_side = fe.side;
1507
1508 if fe.show_hidden {
1510 self.pending_file_explorer_show_hidden = Some(true);
1511 }
1512 if fe.show_gitignored {
1513 self.pending_file_explorer_show_gitignored = Some(true);
1514 }
1515
1516 if self.file_explorer_visible && self.file_explorer.is_none() {
1518 self.init_file_explorer();
1519 }
1520 }
1521
1522 pub(crate) fn apply_fresh_session_explorer_default(
1538 &mut self,
1539 opened_files: bool,
1540 workspace_restored: bool,
1541 ) {
1542 if opened_files || workspace_restored {
1543 return;
1544 }
1545 self.file_explorer_visible = true;
1546 if self.file_explorer.is_none() {
1547 self.init_file_explorer();
1548 }
1549 }
1550
1551 fn open_workspace_files(
1554 &mut self,
1555 split_states: &HashMap<usize, SerializedSplitViewState>,
1556 ) -> HashMap<PathBuf, BufferId> {
1557 let file_paths = collect_file_paths_from_states(split_states);
1558 tracing::debug!(
1559 "Workspace has {} files to restore: {:?}",
1560 file_paths.len(),
1561 file_paths
1562 );
1563 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
1564 for rel_path in file_paths {
1565 let abs_path = self.root.join(&rel_path);
1566 tracing::trace!(
1567 "Checking file: {:?} (exists: {})",
1568 abs_path,
1569 abs_path.exists()
1570 );
1571 if abs_path.exists() {
1572 match self.open_file_internal(&abs_path) {
1573 Ok(buffer_id) => {
1574 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
1575 path_to_buffer.insert(rel_path, buffer_id);
1576 }
1577 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
1578 }
1579 } else {
1580 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
1581 }
1582 }
1583 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
1584 path_to_buffer
1585 }
1586
1587 fn restore_external_files(
1589 &mut self,
1590 external_files: &[PathBuf],
1591 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
1592 ) {
1593 if external_files.is_empty() {
1594 return;
1595 }
1596 tracing::debug!(
1597 "Restoring {} external files: {:?}",
1598 external_files.len(),
1599 external_files
1600 );
1601 for abs_path in external_files {
1602 if !abs_path.exists() {
1603 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
1604 continue;
1605 }
1606 match self.open_file_internal(abs_path) {
1607 Ok(buffer_id) => {
1608 path_to_buffer.insert(abs_path.clone(), buffer_id);
1609 tracing::debug!(
1610 "Restored external file {:?} as buffer {:?}",
1611 abs_path,
1612 buffer_id
1613 );
1614 }
1615 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1616 }
1617 }
1618 }
1619
1620 fn apply_read_only_flags(
1623 &mut self,
1624 read_only_files: &[PathBuf],
1625 path_to_buffer: &HashMap<PathBuf, BufferId>,
1626 ) {
1627 for ro_path in read_only_files {
1628 let buffer_id = path_to_buffer
1629 .get(ro_path)
1630 .copied()
1631 .or_else(|| path_to_buffer.get(&self.root.join(ro_path)).copied());
1632 if let Some(id) = buffer_id {
1633 self.mark_buffer_read_only(id, true);
1634 }
1635 }
1636 }
1637
1638 pub(crate) fn has_any_virtual_buffer(&self) -> bool {
1643 self.buffer_metadata
1644 .values()
1645 .any(|m| matches!(m.kind, crate::app::types::BufferKind::Virtual { .. }))
1646 }
1647
1648 pub(crate) fn save_all_global_file_states(&self) {
1651 for (leaf_id, view_state) in self
1652 .buffers
1653 .splits()
1654 .map(|(_, vs)| vs)
1655 .expect("window must have a populated split layout")
1656 {
1657 let active_buffer = self
1658 .buffers
1659 .splits()
1660 .map(|(mgr, _)| mgr)
1661 .expect("window must have a populated split layout")
1662 .root()
1663 .get_leaves_with_rects(ratatui::layout::Rect::default())
1664 .into_iter()
1665 .find(|(sid, _, _)| *sid == *leaf_id)
1666 .map(|(_, buffer_id, _)| buffer_id);
1667
1668 if let Some(buffer_id) = active_buffer {
1669 self.save_buffer_file_state(buffer_id, view_state);
1670 }
1671 }
1672 }
1673
1674 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
1676 let abs_path = match self.buffer_metadata.get(&buffer_id) {
1677 Some(metadata) => match metadata.file_path() {
1678 Some(path) => path.to_path_buf(),
1679 None => return,
1680 },
1681 None => return,
1682 };
1683
1684 let primary_cursor = view_state.cursors.primary();
1685 let file_state = SerializedFileState {
1686 cursor: SerializedCursor {
1687 position: primary_cursor.position,
1688 anchor: primary_cursor.anchor,
1689 sticky_column: primary_cursor.sticky_column,
1690 },
1691 additional_cursors: view_state
1692 .cursors
1693 .iter()
1694 .skip(1)
1695 .map(|(_, cursor)| SerializedCursor {
1696 position: cursor.position,
1697 anchor: cursor.anchor,
1698 sticky_column: cursor.sticky_column,
1699 })
1700 .collect(),
1701 scroll: SerializedScroll {
1702 top_byte: view_state.viewport.top_byte,
1703 top_view_line_offset: view_state.viewport.top_view_line_offset,
1704 left_column: view_state.viewport.left_column,
1705 },
1706 view_mode: Default::default(),
1707 compose_width: None,
1708 line_numbers: None,
1711 line_wrap: None,
1712 plugin_state: std::collections::HashMap::new(),
1713 folds: Vec::new(),
1714 };
1715
1716 PersistedFileWorkspace::save(&abs_path, file_state);
1717 }
1718
1719 pub(crate) fn sync_terminal_backing_files(&self) {
1722 use std::io::BufWriter;
1723
1724 let terminals_to_sync: Vec<_> = self
1725 .terminal_buffers
1726 .values()
1727 .map(|tb| tb.terminal_id)
1728 .filter_map(|terminal_id| {
1729 self.terminal_backing_files
1730 .get(&terminal_id)
1731 .map(|path| (terminal_id, path.clone()))
1732 })
1733 .collect();
1734
1735 for (terminal_id, backing_path) in terminals_to_sync {
1736 if let Some(handle) = self.terminal_manager.get(terminal_id) {
1737 if let Ok(mut state) = handle.state.lock() {
1738 if let Ok(mut file) = crate::app::terminal::terminal_backing_fs()
1743 .open_file_for_append(&backing_path)
1744 {
1745 let mut writer = BufWriter::new(&mut *file);
1746 if let Err(e) = state.flush_new_scrollback(&mut writer) {
1747 tracing::warn!(
1748 "Failed to flush terminal {:?} scrollback: {}",
1749 terminal_id,
1750 e
1751 );
1752 }
1753 }
1754
1755 if let Ok(mut file) = crate::app::terminal::terminal_backing_fs()
1756 .open_file_for_append(&backing_path)
1757 {
1758 let mut writer = BufWriter::new(&mut *file);
1759 if let Err(e) = state.append_visible_screen(&mut writer) {
1760 tracing::warn!(
1761 "Failed to sync terminal {:?} to backing file: {}",
1762 terminal_id,
1763 e
1764 );
1765 }
1766 }
1767 }
1768 }
1769 }
1770 }
1771
1772 pub(crate) fn create_unnamed_recovery_buffer(
1776 &mut self,
1777 text: &str,
1778 recovery_id: String,
1779 display_name: String,
1780 ) -> BufferId {
1781 let buffer_id = self.alloc_buffer_id();
1782 let mut state = EditorState::new(
1783 self.terminal_width,
1784 self.terminal_height,
1785 self.resources.config.editor.large_file_threshold_bytes as usize,
1786 std::sync::Arc::clone(&self.authority().filesystem),
1787 );
1788 state
1789 .margins
1790 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1791 state.buffer.set_default_line_ending(
1792 self.resources
1793 .config
1794 .editor
1795 .default_line_ending
1796 .to_line_ending(),
1797 );
1798 state.buffer.insert(0, text);
1799 state.buffer.set_modified(true);
1800 state.buffer.set_recovery_pending(false);
1801 self.buffers.insert(buffer_id, state);
1802
1803 let mut log = crate::model::event::EventLog::new();
1804 log.clear_saved_position();
1805 self.event_logs.insert(buffer_id, log);
1806
1807 let mut meta = crate::app::types::BufferMetadata::new();
1808 meta.recovery_id = Some(recovery_id);
1809 meta.display_name = display_name;
1810 self.buffer_metadata.insert(buffer_id, meta);
1811
1812 buffer_id
1813 }
1814
1815 pub(crate) fn seed_initial_layout(&mut self) {
1819 if self.buffers.splits().is_some() && self.buffers.len() > 0 {
1820 return;
1821 }
1822 let buf = self.alloc_buffer_id();
1823 let mut state = EditorState::new(
1824 self.terminal_width,
1825 self.terminal_height,
1826 self.resources.config.editor.large_file_threshold_bytes as usize,
1827 std::sync::Arc::clone(&self.authority().filesystem),
1828 );
1829 state
1830 .margins
1831 .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1832 state.buffer.set_default_line_ending(
1833 self.resources
1834 .config
1835 .editor
1836 .default_line_ending
1837 .to_line_ending(),
1838 );
1839 let manager = crate::view::split::SplitManager::new(buf);
1840 let active_leaf = manager.active_split();
1841 let mut view_states = HashMap::new();
1842 view_states.insert(
1843 active_leaf,
1844 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buf),
1845 );
1846 self.buffers.set_splits((manager, view_states));
1847 self.buffers.insert(buf, state);
1848 self.buffer_metadata
1849 .insert(buf, crate::app::types::BufferMetadata::new());
1850 self.event_logs
1851 .insert(buf, crate::model::event::EventLog::new());
1852 }
1853
1854 pub(crate) fn sync_lsp_after_recovery_replay(&mut self, buffer_id: BufferId) {
1858 let Some(text) = self
1859 .buffers
1860 .get(&buffer_id)
1861 .and_then(|state| state.buffer.to_string())
1862 else {
1863 return;
1864 };
1865 let full_change = lsp_types::TextDocumentContentChangeEvent {
1866 range: None,
1867 range_length: None,
1868 text,
1869 };
1870 self.send_lsp_changes_for_buffer(buffer_id, vec![full_change]);
1871 }
1872
1873 fn restore_unnamed_buffers(
1879 &mut self,
1880 unnamed_buffers: &[UnnamedBufferRef],
1881 ) -> HashMap<String, BufferId> {
1882 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1883 if !self.resources.config.editor.hot_exit || unnamed_buffers.is_empty() {
1884 return unnamed_buffer_map;
1885 }
1886 tracing::debug!(
1887 "Restoring {} unnamed buffers from recovery",
1888 unnamed_buffers.len()
1889 );
1890 for unnamed_ref in unnamed_buffers {
1891 let entries = match self
1892 .resources
1893 .recovery_service
1894 .lock()
1895 .unwrap()
1896 .list_recoverable()
1897 {
1898 Ok(e) => e,
1899 Err(e) => {
1900 tracing::warn!("Failed to list recovery entries: {}", e);
1901 continue;
1902 }
1903 };
1904 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1905 tracing::debug!(
1906 "Recovery file not found for unnamed buffer {}",
1907 unnamed_ref.recovery_id
1908 );
1909 continue;
1910 };
1911 let loaded = self
1912 .resources
1913 .recovery_service
1914 .lock()
1915 .unwrap()
1916 .load_recovery(entry);
1917 match loaded {
1918 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1919 let text = String::from_utf8_lossy(&content).into_owned();
1920 let buffer_id = self.create_unnamed_recovery_buffer(
1921 &text,
1922 unnamed_ref.recovery_id.clone(),
1923 unnamed_ref.display_name.clone(),
1924 );
1925 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1926 tracing::info!(
1927 "Restored unnamed buffer '{}' (recovery_id={})",
1928 unnamed_ref.display_name,
1929 unnamed_ref.recovery_id
1930 );
1931 }
1932 Ok(other) => {
1933 tracing::warn!(
1934 "Unexpected recovery result for unnamed buffer {}: {:?}",
1935 unnamed_ref.recovery_id,
1936 std::mem::discriminant(&other)
1937 );
1938 }
1939 Err(e) => {
1940 tracing::warn!(
1941 "Failed to load recovery for unnamed buffer {}: {}",
1942 unnamed_ref.recovery_id,
1943 e
1944 );
1945 }
1946 }
1947 }
1948 unnamed_buffer_map
1949 }
1950
1951 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1955 if !self.resources.config.editor.hot_exit {
1956 return;
1957 }
1958 let entries = self
1959 .resources
1960 .recovery_service
1961 .lock()
1962 .unwrap()
1963 .list_recoverable()
1964 .unwrap_or_default();
1965 if entries.is_empty() {
1966 return;
1967 }
1968 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1969 for buffer_id in buffer_ids {
1970 let file_path = self
1971 .buffers
1972 .get(&buffer_id)
1973 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1974 let Some(file_path) = file_path else { continue };
1975
1976 let recovery_id = self
1977 .resources
1978 .recovery_service
1979 .lock()
1980 .unwrap()
1981 .get_buffer_id(Some(&file_path));
1982 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1983 continue;
1984 };
1985 let loaded = self
1986 .resources
1987 .recovery_service
1988 .lock()
1989 .unwrap()
1990 .load_recovery(entry);
1991 match loaded {
1992 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1993 let mut mutated = false;
1994 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1995 let current_len = state.buffer.total_bytes();
1996 let text = String::from_utf8_lossy(&content).into_owned();
1997 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1998 let current_text = current
1999 .as_ref()
2000 .map(|b| String::from_utf8_lossy(b).into_owned());
2001 if current_text.as_deref() != Some(&text) {
2002 state.buffer.delete(0..current_len);
2003 state.buffer.insert(0, &text);
2004 state.buffer.set_modified(true);
2005 state.buffer.set_recovery_pending(false);
2006 mutated = true;
2007 tracing::info!(
2008 "Restored unsaved changes for {:?} from hot exit recovery",
2009 file_path
2010 );
2011 }
2012 }
2013 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
2014 log.clear_saved_position();
2015 }
2016 if mutated {
2017 self.sync_lsp_after_recovery_replay(buffer_id);
2018 }
2019 }
2020 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
2021 chunks, ..
2022 }) => {
2023 let mut mutated = false;
2024 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2025 for chunk in chunks.into_iter().rev() {
2026 let text = String::from_utf8_lossy(&chunk.content).into_owned();
2027 if chunk.original_len > 0 {
2028 state
2029 .buffer
2030 .delete(chunk.offset..chunk.offset + chunk.original_len);
2031 }
2032 state.buffer.insert(chunk.offset, &text);
2033 }
2034 state.buffer.set_modified(true);
2035 state.buffer.set_recovery_pending(false);
2036 mutated = true;
2037 tracing::info!(
2038 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
2039 file_path
2040 );
2041 }
2042 if let Some(log) = self.event_logs.get_mut(&buffer_id) {
2043 log.clear_saved_position();
2044 }
2045 if mutated {
2046 self.sync_lsp_after_recovery_replay(buffer_id);
2047 }
2048 }
2049 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
2050 original_path,
2051 ..
2052 }) => {
2053 let name = original_path
2054 .file_name()
2055 .unwrap_or_default()
2056 .to_string_lossy();
2057 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
2058 self.set_status_message(format!(
2059 "{} changed on disk; unsaved changes not restored",
2060 name
2061 ));
2062 }
2063 Ok(_) => {} Err(e) => {
2065 tracing::debug!(
2066 "Failed to load hot exit recovery for {:?}: {}",
2067 file_path,
2068 e
2069 );
2070 }
2071 }
2072 }
2073 }
2074
2075 pub(crate) fn apply_workspace_layout(
2091 &mut self,
2092 workspace: &Workspace,
2093 session_name: Option<&str>,
2094 ) {
2095 tracing::debug!(
2096 "Applying workspace layout with {} split states",
2097 workspace.split_states.len()
2098 );
2099
2100 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
2103 self.mouse_enabled = mouse_enabled;
2104 }
2105
2106 self.restore_search_options(&workspace.search_options);
2107 self.restore_prompt_histories(&workspace.histories);
2108 self.restore_file_explorer_settings(&workspace.file_explorer);
2109
2110 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
2113
2114 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
2115 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
2116 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
2117
2118 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
2119
2120 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
2121 self.restore_split_node(
2122 &workspace.split_layout,
2123 &path_to_buffer,
2124 &terminal_buffer_map,
2125 &unnamed_buffer_map,
2126 &workspace.split_states,
2127 &mut split_id_map,
2128 true,
2129 );
2130
2131 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
2132 self.buffers
2133 .split_manager_mut()
2134 .expect("window must have a populated split layout")
2135 .set_active_split(LeafId(new_active_split));
2136 }
2137
2138 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
2139 self.clean_orphaned_buffers();
2140 self.log_restore_summary(session_name);
2141
2142 self.restore_hot_exit_changes(&path_to_buffer);
2144 }
2145
2146 pub(crate) fn from_workspace(
2152 id: fresh_core::WindowId,
2153 label: impl Into<String>,
2154 root: PathBuf,
2155 authority: crate::services::authority::Authority,
2156 resources: crate::app::window_resources::WindowResources,
2157 workspace: &Workspace,
2158 ) -> Self {
2159 let mut window = Self::new(id, label, root, authority, resources);
2160 window.seed_initial_layout();
2161 window.apply_workspace_layout(workspace, None);
2162 window
2163 }
2164
2165 pub(crate) fn capture_workspace(&self) -> Workspace {
2171 tracing::debug!("Capturing workspace for {:?}", self.root);
2172
2173 let mut terminals = Vec::new();
2174 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
2175 let mut seen = HashSet::new();
2176 for terminal_id in self.terminal_buffers.values().map(|tb| tb.terminal_id) {
2177 if seen.insert(terminal_id) {
2178 let command = self.terminal_commands.get(&terminal_id).cloned();
2179 if self.ephemeral_terminals.contains(&terminal_id) && command.is_none() {
2187 continue;
2188 }
2189 let idx = terminals.len();
2190 terminal_indices.insert(terminal_id, idx);
2191 let handle = self.terminal_manager.get(terminal_id);
2192 let (cols, rows) = handle
2193 .map(|h| h.size())
2194 .unwrap_or((self.terminal_width, self.terminal_height));
2195 let cwd = handle.and_then(|h| h.cwd());
2196 let shell = handle
2197 .map(|h| h.shell().to_string())
2198 .unwrap_or_else(crate::services::terminal::detect_shell);
2199 let log_path = self
2200 .terminal_log_files
2201 .get(&terminal_id)
2202 .cloned()
2203 .unwrap_or_else(|| {
2204 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2205 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
2206 });
2207 let backing_path = self
2208 .terminal_backing_files
2209 .get(&terminal_id)
2210 .cloned()
2211 .unwrap_or_else(|| {
2212 let root = self.resources.dir_context.terminal_dir_for(&self.root);
2213 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
2214 });
2215
2216 let agent_resume = self
2217 .terminal_resume_commands
2218 .get(&terminal_id)
2219 .filter(|argv| !argv.is_empty())
2220 .map(|argv| crate::workspace::AgentResume { argv: argv.clone() });
2221 terminals.push(SerializedTerminalWorkspace {
2222 terminal_index: idx,
2223 cwd,
2224 shell,
2225 cols,
2226 rows,
2227 log_path,
2228 backing_path,
2229 command,
2230 agent_resume,
2231 });
2232 }
2233 }
2234
2235 let (mgr, view_states) = self
2236 .buffers
2237 .splits()
2238 .expect("window must have a populated split layout");
2239
2240 let terminal_id_map: HashMap<BufferId, TerminalId> = self
2243 .terminal_buffers
2244 .iter()
2245 .map(|(b, tb)| (*b, tb.terminal_id))
2246 .collect();
2247
2248 let split_layout = serialize_split_node(
2249 mgr.root(),
2250 &self.buffer_metadata,
2251 &self.root,
2252 &terminal_id_map,
2253 &terminal_indices,
2254 mgr.labels(),
2255 );
2256
2257 let active_buffers: HashMap<LeafId, BufferId> = mgr
2258 .root()
2259 .get_leaves_with_rects(ratatui::layout::Rect::default())
2260 .into_iter()
2261 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
2262 .collect();
2263
2264 let mut split_states = HashMap::new();
2265 for (leaf_id, view_state) in view_states {
2266 let active_buffer = active_buffers.get(leaf_id).copied();
2267 let serialized = serialize_split_view_state(
2268 view_state,
2269 self.buffers.as_map(),
2270 &self.buffer_metadata,
2271 &self.root,
2272 active_buffer,
2273 &terminal_id_map,
2274 &terminal_indices,
2275 );
2276 split_states.insert(leaf_id.0 .0, serialized);
2277 }
2278
2279 let file_explorer = if let Some(explorer) = self.file_explorer.as_ref() {
2280 let expanded_dirs = get_expanded_dirs(explorer, &self.root);
2281 FileExplorerState {
2282 visible: self.file_explorer_visible,
2283 width: self.file_explorer_width,
2284 side: self.file_explorer_side,
2285 expanded_dirs,
2286 scroll_offset: explorer.get_scroll_offset(),
2287 show_hidden: explorer.ignore_patterns().show_hidden(),
2288 show_gitignored: explorer.ignore_patterns().show_gitignored(),
2289 }
2290 } else {
2291 FileExplorerState {
2292 visible: self.file_explorer_visible,
2293 width: self.file_explorer_width,
2294 side: self.file_explorer_side,
2295 expanded_dirs: Vec::new(),
2296 scroll_offset: 0,
2297 show_hidden: false,
2298 show_gitignored: false,
2299 }
2300 };
2301
2302 let cfg = &self.resources.config.editor;
2303 let config_overrides = WorkspaceConfigOverrides {
2304 line_numbers: Some(cfg.line_numbers),
2305 relative_line_numbers: Some(cfg.relative_line_numbers),
2306 line_wrap: Some(cfg.line_wrap),
2307 syntax_highlighting: Some(cfg.syntax_highlighting),
2308 enable_inlay_hints: Some(cfg.enable_inlay_hints),
2309 mouse_enabled: Some(self.mouse_enabled),
2310 menu_bar_hidden: None,
2311 };
2312
2313 let histories = WorkspaceHistories {
2314 search: self
2315 .prompt_histories
2316 .get("search")
2317 .map(|h| h.items().to_vec())
2318 .unwrap_or_default(),
2319 replace: self
2320 .prompt_histories
2321 .get("replace")
2322 .map(|h| h.items().to_vec())
2323 .unwrap_or_default(),
2324 command_palette: Vec::new(),
2325 goto_line: self
2326 .prompt_histories
2327 .get("goto_line")
2328 .map(|h| h.items().to_vec())
2329 .unwrap_or_default(),
2330 open_file: Vec::new(),
2331 };
2332
2333 let search_options = SearchOptions {
2334 case_sensitive: self.search_case_sensitive,
2335 whole_word: self.search_whole_word,
2336 use_regex: self.search_use_regex,
2337 confirm_each: self.search_confirm_each,
2338 };
2339
2340 let bookmarks = serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.root);
2341
2342 let external_files: Vec<PathBuf> = self
2343 .buffer_metadata
2344 .values()
2345 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2346 .filter_map(|meta| meta.file_path())
2347 .filter(|abs_path| abs_path.strip_prefix(&self.root).is_err())
2348 .cloned()
2349 .collect();
2350
2351 let read_only_files: Vec<PathBuf> = self
2352 .buffer_metadata
2353 .values()
2354 .filter(|meta| !meta.hidden_from_tabs && !meta.is_virtual())
2355 .filter(|meta| meta.read_only)
2356 .filter_map(|meta| meta.file_path().cloned())
2357 .filter(|p| !p.as_os_str().is_empty())
2358 .map(|p| {
2359 p.strip_prefix(&self.root)
2360 .map(|rel| rel.to_path_buf())
2361 .unwrap_or(p)
2362 })
2363 .collect();
2364
2365 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.resources.config.editor.hot_exit {
2366 self.buffer_metadata
2367 .iter()
2368 .filter_map(|(buffer_id, meta)| {
2369 let path = meta.file_path()?;
2370 if !path.as_os_str().is_empty() {
2371 return None;
2372 }
2373 if meta.hidden_from_tabs || meta.is_virtual() {
2374 return None;
2375 }
2376 let state = self.buffers.get(buffer_id)?;
2377 if state.buffer.total_bytes() == 0 {
2378 return None;
2379 }
2380 let recovery_id = meta.recovery_id.clone()?;
2381 Some(UnnamedBufferRef {
2382 recovery_id,
2383 display_name: meta.display_name.clone(),
2384 })
2385 })
2386 .collect()
2387 } else {
2388 Vec::new()
2389 };
2390
2391 Workspace {
2392 version: WORKSPACE_VERSION,
2393 working_dir: self.root.clone(),
2394 split_layout,
2395 active_split_id: SplitId::from(mgr.active_split()).0,
2396 split_states,
2397 config_overrides,
2398 file_explorer,
2399 histories,
2400 search_options,
2401 bookmarks,
2402 terminals,
2403 external_files,
2404 read_only_files,
2405 unnamed_buffers,
2406 plugin_global_state: HashMap::new(),
2407 saved_at: std::time::SystemTime::now()
2408 .duration_since(std::time::UNIX_EPOCH)
2409 .unwrap_or_default()
2410 .as_secs(),
2411 label: Some(self.label.clone()),
2414 session_plugin_state: self.plugin_state.clone(),
2415 authority_spec: self.authority_spec.clone(),
2417 }
2418 }
2419}
2420
2421fn get_first_leaf_buffer(
2423 node: &SerializedSplitNode,
2424 path_to_buffer: &HashMap<PathBuf, BufferId>,
2425 terminal_buffers: &HashMap<usize, BufferId>,
2426 unnamed_buffers: &HashMap<String, BufferId>,
2427) -> Option<BufferId> {
2428 match node {
2429 SerializedSplitNode::Leaf {
2430 file_path,
2431 unnamed_recovery_id,
2432 ..
2433 } => file_path
2434 .as_ref()
2435 .and_then(|p| path_to_buffer.get(p).copied())
2436 .or_else(|| {
2437 unnamed_recovery_id
2438 .as_ref()
2439 .and_then(|id| unnamed_buffers.get(id).copied())
2440 }),
2441 SerializedSplitNode::Terminal { terminal_index, .. } => {
2442 terminal_buffers.get(terminal_index).copied()
2443 }
2444 SerializedSplitNode::Split { first, .. } => {
2445 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2446 }
2447 }
2448}
2449
2450fn serialize_split_node(
2455 node: &SplitNode,
2456 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2457 working_dir: &Path,
2458 terminal_buffers: &HashMap<BufferId, TerminalId>,
2459 terminal_indices: &HashMap<TerminalId, usize>,
2460 split_labels: &HashMap<SplitId, String>,
2461) -> SerializedSplitNode {
2462 serialize_split_node_pruned(
2463 node,
2464 buffer_metadata,
2465 working_dir,
2466 terminal_buffers,
2467 terminal_indices,
2468 split_labels,
2469 )
2470 .unwrap_or({
2471 SerializedSplitNode::Leaf {
2474 file_path: None,
2475 split_id: 0,
2476 label: None,
2477 unnamed_recovery_id: None,
2478 role: None,
2479 }
2480 })
2481}
2482
2483fn serialize_split_node_pruned(
2490 node: &SplitNode,
2491 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2492 working_dir: &Path,
2493 terminal_buffers: &HashMap<BufferId, TerminalId>,
2494 terminal_indices: &HashMap<TerminalId, usize>,
2495 split_labels: &HashMap<SplitId, String>,
2496) -> Option<SerializedSplitNode> {
2497 match node {
2498 SplitNode::Grouped { layout, .. } => {
2499 serialize_split_node_pruned(
2503 layout,
2504 buffer_metadata,
2505 working_dir,
2506 terminal_buffers,
2507 terminal_indices,
2508 split_labels,
2509 )
2510 }
2511 SplitNode::Leaf {
2512 buffer_id,
2513 split_id,
2514 role,
2515 } => {
2516 let raw_split_id: SplitId = (*split_id).into();
2517 let label = split_labels.get(&raw_split_id).cloned();
2518 let role = *role;
2519
2520 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2521 if let Some(index) = terminal_indices.get(terminal_id) {
2522 return Some(SerializedSplitNode::Terminal {
2523 terminal_index: *index,
2524 split_id: raw_split_id.0,
2525 label,
2526 role,
2527 });
2528 }
2529 }
2530
2531 let meta = buffer_metadata.get(buffer_id);
2532
2533 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2537 return None;
2538 }
2539
2540 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2541 if abs_path.as_os_str().is_empty() {
2542 None } else {
2544 abs_path
2545 .strip_prefix(working_dir)
2546 .ok()
2547 .map(|p| p.to_path_buf())
2548 }
2549 });
2550
2551 let unnamed_recovery_id = if file_path.is_none() {
2554 meta.and_then(|m| m.recovery_id.clone())
2555 } else {
2556 None
2557 };
2558
2559 Some(SerializedSplitNode::Leaf {
2560 file_path,
2561 split_id: raw_split_id.0,
2562 label,
2563 unnamed_recovery_id,
2564 role,
2565 })
2566 }
2567 SplitNode::Split {
2568 direction,
2569 first,
2570 second,
2571 ratio,
2572 split_id,
2573 ..
2574 } => {
2575 let raw_split_id: SplitId = (*split_id).into();
2576 let first = serialize_split_node_pruned(
2577 first,
2578 buffer_metadata,
2579 working_dir,
2580 terminal_buffers,
2581 terminal_indices,
2582 split_labels,
2583 );
2584 let second = serialize_split_node_pruned(
2585 second,
2586 buffer_metadata,
2587 working_dir,
2588 terminal_buffers,
2589 terminal_indices,
2590 split_labels,
2591 );
2592 match (first, second) {
2593 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2594 direction: match direction {
2595 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2596 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2597 },
2598 first: Box::new(f),
2599 second: Box::new(s),
2600 ratio: *ratio,
2601 split_id: raw_split_id.0,
2602 }),
2603 (Some(only), None) | (None, Some(only)) => Some(only),
2606 (None, None) => None,
2607 }
2608 }
2609 }
2610}
2611
2612fn serialize_split_view_state(
2613 view_state: &crate::view::split::SplitViewState,
2614 buffers: &HashMap<BufferId, EditorState>,
2615 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2616 working_dir: &Path,
2617 active_buffer: Option<BufferId>,
2618 terminal_buffers: &HashMap<BufferId, TerminalId>,
2619 terminal_indices: &HashMap<TerminalId, usize>,
2620) -> SerializedSplitViewState {
2621 let mut open_tabs = Vec::new();
2622 let mut open_files = Vec::new();
2623 let mut active_tab_index = None;
2624
2625 for buffer_id in view_state.buffer_tab_ids() {
2627 let buffer_id = &buffer_id;
2628 let tab_index = open_tabs.len();
2629 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2630 if let Some(idx) = terminal_indices.get(terminal_id) {
2631 open_tabs.push(SerializedTabRef::Terminal(*idx));
2632 if Some(*buffer_id) == active_buffer {
2633 active_tab_index = Some(tab_index);
2634 }
2635 continue;
2636 }
2637 }
2638
2639 if let Some(meta) = buffer_metadata.get(buffer_id) {
2640 if let Some(abs_path) = meta.file_path() {
2641 if abs_path.as_os_str().is_empty() {
2642 if let Some(ref recovery_id) = meta.recovery_id {
2644 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2645 if Some(*buffer_id) == active_buffer {
2646 active_tab_index = Some(tab_index);
2647 }
2648 }
2649 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2650 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2651 open_files.push(rel_path.to_path_buf());
2652 if Some(*buffer_id) == active_buffer {
2653 active_tab_index = Some(tab_index);
2654 }
2655 } else {
2656 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2658 if Some(*buffer_id) == active_buffer {
2659 active_tab_index = Some(tab_index);
2660 }
2661 }
2662 }
2663 }
2664 }
2665
2666 let active_file_index = active_tab_index
2668 .and_then(|idx| open_tabs.get(idx))
2669 .and_then(|tab| match tab {
2670 SerializedTabRef::File(path) => {
2671 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2672 }
2673 _ => None,
2674 })
2675 .unwrap_or(0);
2676
2677 let mut file_states = HashMap::new();
2679 for (buffer_id, buf_state) in &view_state.keyed_states {
2680 let Some(meta) = buffer_metadata.get(buffer_id) else {
2681 continue;
2682 };
2683 let Some(abs_path) = meta.file_path() else {
2684 continue;
2685 };
2686
2687 let state_key = if abs_path.as_os_str().is_empty() {
2689 if let Some(ref recovery_id) = meta.recovery_id {
2691 PathBuf::from(format!("__unnamed__{}", recovery_id))
2692 } else {
2693 continue;
2694 }
2695 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2696 rp.to_path_buf()
2697 } else {
2698 abs_path.to_path_buf()
2700 };
2701
2702 let primary_cursor = buf_state.cursors.primary();
2703 let folds = buffers
2704 .get(buffer_id)
2705 .map(|state| {
2706 buf_state
2707 .folds
2708 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2709 .into_iter()
2710 .map(|range| SerializedFoldRange {
2711 header_line: range.header_line,
2712 end_line: range.end_line,
2713 placeholder: range.placeholder,
2714 header_text: range.header_text,
2715 })
2716 .collect::<Vec<_>>()
2717 })
2718 .unwrap_or_default();
2719
2720 file_states.insert(
2721 state_key,
2722 SerializedFileState {
2723 cursor: SerializedCursor {
2724 position: primary_cursor.position,
2725 anchor: primary_cursor.anchor,
2726 sticky_column: primary_cursor.sticky_column,
2727 },
2728 additional_cursors: buf_state
2729 .cursors
2730 .iter()
2731 .skip(1) .map(|(_, cursor)| SerializedCursor {
2733 position: cursor.position,
2734 anchor: cursor.anchor,
2735 sticky_column: cursor.sticky_column,
2736 })
2737 .collect(),
2738 scroll: SerializedScroll {
2739 top_byte: buf_state.viewport.top_byte,
2740 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2741 left_column: buf_state.viewport.left_column,
2742 },
2743 view_mode: match buf_state.view_mode {
2744 ViewMode::Source => SerializedViewMode::Source,
2745 ViewMode::PageView => SerializedViewMode::PageView,
2746 },
2747 compose_width: buf_state.compose_width,
2748 line_numbers: buf_state.line_numbers_override,
2749 line_wrap: buf_state.line_wrap_override,
2750 plugin_state: buf_state.plugin_state.clone(),
2751 folds,
2752 },
2753 );
2754 }
2755
2756 let active_view_mode = active_buffer
2758 .and_then(|id| view_state.keyed_states.get(&id))
2759 .map(|bs| match bs.view_mode {
2760 ViewMode::Source => SerializedViewMode::Source,
2761 ViewMode::PageView => SerializedViewMode::PageView,
2762 })
2763 .unwrap_or(SerializedViewMode::Source);
2764 let active_compose_width = active_buffer
2765 .and_then(|id| view_state.keyed_states.get(&id))
2766 .and_then(|bs| bs.compose_width);
2767
2768 SerializedSplitViewState {
2769 open_tabs,
2770 active_tab_index,
2771 open_files,
2772 active_file_index,
2773 file_states,
2774 tab_scroll_offset: view_state.tab_scroll_offset,
2775 view_mode: active_view_mode,
2776 compose_width: active_compose_width,
2777 }
2778}
2779
2780fn serialize_bookmarks(
2781 bookmarks: &BookmarkState,
2782 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2783 working_dir: &Path,
2784) -> HashMap<char, SerializedBookmark> {
2785 bookmarks
2786 .iter()
2787 .filter_map(|(key, bookmark)| {
2788 buffer_metadata
2789 .get(&bookmark.buffer_id)
2790 .and_then(|meta| meta.file_path())
2791 .and_then(|abs_path| {
2792 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2793 (
2794 key,
2795 SerializedBookmark {
2796 file_path: rel_path.to_path_buf(),
2797 position: bookmark.position,
2798 },
2799 )
2800 })
2801 })
2802 })
2803 .collect()
2804}
2805
2806fn collect_file_paths_from_states(
2808 split_states: &HashMap<usize, SerializedSplitViewState>,
2809) -> Vec<PathBuf> {
2810 let mut paths = Vec::new();
2811 for state in split_states.values() {
2812 if !state.open_tabs.is_empty() {
2813 for tab in &state.open_tabs {
2814 if let SerializedTabRef::File(path) = tab {
2815 if !paths.contains(path) {
2816 paths.push(path.clone());
2817 }
2818 }
2819 }
2820 } else {
2821 for path in &state.open_files {
2822 if !paths.contains(path) {
2823 paths.push(path.clone());
2824 }
2825 }
2826 }
2827 }
2828 paths
2829}
2830
2831fn get_expanded_dirs(
2833 explorer: &crate::view::file_tree::FileTreeView,
2834 working_dir: &Path,
2835) -> Vec<PathBuf> {
2836 let mut expanded = Vec::new();
2837 let tree = explorer.tree();
2838
2839 for node in tree.all_nodes() {
2841 if node.is_expanded() && node.is_dir() {
2842 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2844 expanded.push(rel_path.to_path_buf());
2845 }
2846 }
2847 }
2848
2849 expanded
2850}