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, Workspace, WorkspaceConfigOverrides, WorkspaceError, WorkspaceHistories,
40 WORKSPACE_VERSION,
41};
42
43use super::types::Bookmark;
44use super::Editor;
45
46pub struct WorkspaceTracker {
50 dirty: bool,
52 last_save: Instant,
54 save_interval: std::time::Duration,
56 enabled: bool,
58}
59
60impl WorkspaceTracker {
61 pub fn new(enabled: bool) -> Self {
63 Self {
64 dirty: false,
65 last_save: Instant::now(),
66 save_interval: std::time::Duration::from_secs(5),
67 enabled,
68 }
69 }
70
71 pub fn is_enabled(&self) -> bool {
73 self.enabled
74 }
75
76 pub fn mark_dirty(&mut self) {
78 if self.enabled {
79 self.dirty = true;
80 }
81 }
82
83 pub fn should_save(&self) -> bool {
85 self.enabled && self.dirty && self.last_save.elapsed() >= self.save_interval
86 }
87
88 pub fn record_save(&mut self) {
90 self.dirty = false;
91 self.last_save = Instant::now();
92 }
93
94 pub fn is_dirty(&self) -> bool {
96 self.dirty
97 }
98}
99
100impl Editor {
101 pub fn capture_workspace(&self) -> Workspace {
103 tracing::debug!("Capturing workspace for {:?}", self.working_dir);
104
105 let mut terminals = Vec::new();
107 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
108 let mut seen = HashSet::new();
109 for terminal_id in self.terminal_buffers.values().copied() {
110 if seen.insert(terminal_id) {
111 let idx = terminals.len();
112 terminal_indices.insert(terminal_id, idx);
113 let handle = self.terminal_manager.get(terminal_id);
114 let (cols, rows) = handle
115 .map(|h| h.size())
116 .unwrap_or((self.terminal_width, self.terminal_height));
117 let cwd = handle.and_then(|h| h.cwd());
118 let shell = handle
119 .map(|h| h.shell().to_string())
120 .unwrap_or_else(crate::services::terminal::detect_shell);
121 let log_path = self
122 .terminal_log_files
123 .get(&terminal_id)
124 .cloned()
125 .unwrap_or_else(|| {
126 let root = self.dir_context.terminal_dir_for(&self.working_dir);
127 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
128 });
129 let backing_path = self
130 .terminal_backing_files
131 .get(&terminal_id)
132 .cloned()
133 .unwrap_or_else(|| {
134 let root = self.dir_context.terminal_dir_for(&self.working_dir);
135 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
136 });
137
138 terminals.push(SerializedTerminalWorkspace {
139 terminal_index: idx,
140 cwd,
141 shell,
142 cols,
143 rows,
144 log_path,
145 backing_path,
146 });
147 }
148 }
149
150 let split_layout = serialize_split_node(
151 self.split_manager.root(),
152 &self.buffer_metadata,
153 &self.working_dir,
154 &self.terminal_buffers,
155 &terminal_indices,
156 self.split_manager.labels(),
157 );
158
159 let active_buffers: HashMap<LeafId, BufferId> = self
162 .split_manager
163 .root()
164 .get_leaves_with_rects(ratatui::layout::Rect::default())
165 .into_iter()
166 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
167 .collect();
168
169 let mut split_states = HashMap::new();
170 for (leaf_id, view_state) in &self.split_view_states {
171 let active_buffer = active_buffers.get(leaf_id).copied();
172 let serialized = serialize_split_view_state(
173 view_state,
174 &self.buffers,
175 &self.buffer_metadata,
176 &self.working_dir,
177 active_buffer,
178 &self.terminal_buffers,
179 &terminal_indices,
180 );
181 tracing::trace!(
182 "Split {:?}: {} open tabs, active_buffer={:?}",
183 leaf_id,
184 serialized.open_tabs.len(),
185 active_buffer
186 );
187 split_states.insert(leaf_id.0 .0, serialized);
188 }
189
190 tracing::debug!(
191 "Captured {} split states, active_split={}",
192 split_states.len(),
193 SplitId::from(self.split_manager.active_split()).0
194 );
195
196 let file_explorer = if let Some(ref explorer) = self.file_explorer {
198 let expanded_dirs = get_expanded_dirs(explorer, &self.working_dir);
200 FileExplorerState {
201 visible: self.file_explorer_visible,
202 width_percent: self.file_explorer_width_percent,
203 expanded_dirs,
204 scroll_offset: explorer.get_scroll_offset(),
205 show_hidden: explorer.ignore_patterns().show_hidden(),
206 show_gitignored: explorer.ignore_patterns().show_gitignored(),
207 }
208 } else {
209 FileExplorerState {
210 visible: self.file_explorer_visible,
211 width_percent: self.file_explorer_width_percent,
212 expanded_dirs: Vec::new(),
213 scroll_offset: 0,
214 show_hidden: false,
215 show_gitignored: false,
216 }
217 };
218
219 let config_overrides = WorkspaceConfigOverrides {
221 line_numbers: Some(self.config.editor.line_numbers),
222 relative_line_numbers: Some(self.config.editor.relative_line_numbers),
223 line_wrap: Some(self.config.editor.line_wrap),
224 syntax_highlighting: Some(self.config.editor.syntax_highlighting),
225 enable_inlay_hints: Some(self.config.editor.enable_inlay_hints),
226 mouse_enabled: Some(self.mouse_enabled),
227 menu_bar_hidden: Some(!self.menu_bar_visible),
228 };
229
230 let histories = WorkspaceHistories {
232 search: self
233 .prompt_histories
234 .get("search")
235 .map(|h| h.items().to_vec())
236 .unwrap_or_default(),
237 replace: self
238 .prompt_histories
239 .get("replace")
240 .map(|h| h.items().to_vec())
241 .unwrap_or_default(),
242 command_palette: Vec::new(), goto_line: self
244 .prompt_histories
245 .get("goto_line")
246 .map(|h| h.items().to_vec())
247 .unwrap_or_default(),
248 open_file: Vec::new(), };
250 tracing::trace!(
251 "Captured histories: {} search, {} replace",
252 histories.search.len(),
253 histories.replace.len()
254 );
255
256 let search_options = SearchOptions {
258 case_sensitive: self.search_case_sensitive,
259 whole_word: self.search_whole_word,
260 use_regex: self.search_use_regex,
261 confirm_each: self.search_confirm_each,
262 };
263
264 let bookmarks =
266 serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.working_dir);
267
268 let external_files: Vec<PathBuf> = self
271 .buffer_metadata
272 .values()
273 .filter_map(|meta| meta.file_path())
274 .filter(|abs_path| abs_path.strip_prefix(&self.working_dir).is_err())
275 .cloned()
276 .collect();
277 if !external_files.is_empty() {
278 tracing::debug!("Captured {} external files", external_files.len());
279 }
280
281 Workspace {
282 version: WORKSPACE_VERSION,
283 working_dir: self.working_dir.clone(),
284 split_layout,
285 active_split_id: SplitId::from(self.split_manager.active_split()).0,
286 split_states,
287 config_overrides,
288 file_explorer,
289 histories,
290 search_options,
291 bookmarks,
292 terminals,
293 external_files,
294 saved_at: std::time::SystemTime::now()
295 .duration_since(std::time::UNIX_EPOCH)
296 .unwrap_or_default()
297 .as_secs(),
298 }
299 }
300
301 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
307 self.sync_all_terminal_backing_files();
309
310 self.save_all_global_file_states();
312
313 let workspace = self.capture_workspace();
314 workspace.save()
315 }
316
317 fn save_all_global_file_states(&self) {
319 for (leaf_id, view_state) in &self.split_view_states {
321 let active_buffer = self
323 .split_manager
324 .root()
325 .get_leaves_with_rects(ratatui::layout::Rect::default())
326 .into_iter()
327 .find(|(sid, _, _)| *sid == *leaf_id)
328 .map(|(_, buffer_id, _)| buffer_id);
329
330 if let Some(buffer_id) = active_buffer {
331 self.save_buffer_file_state(buffer_id, view_state);
332 }
333 }
334 }
335
336 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
338 let abs_path = match self.buffer_metadata.get(&buffer_id) {
340 Some(metadata) => match metadata.file_path() {
341 Some(path) => path.to_path_buf(),
342 None => return, },
344 None => return,
345 };
346
347 let primary_cursor = view_state.cursors.primary();
349 let file_state = SerializedFileState {
350 cursor: SerializedCursor {
351 position: primary_cursor.position,
352 anchor: primary_cursor.anchor,
353 sticky_column: primary_cursor.sticky_column,
354 },
355 additional_cursors: view_state
356 .cursors
357 .iter()
358 .skip(1)
359 .map(|(_, cursor)| SerializedCursor {
360 position: cursor.position,
361 anchor: cursor.anchor,
362 sticky_column: cursor.sticky_column,
363 })
364 .collect(),
365 scroll: SerializedScroll {
366 top_byte: view_state.viewport.top_byte,
367 top_view_line_offset: view_state.viewport.top_view_line_offset,
368 left_column: view_state.viewport.left_column,
369 },
370 view_mode: Default::default(),
371 compose_width: None,
372 plugin_state: std::collections::HashMap::new(),
373 folds: Vec::new(),
374 };
375
376 PersistedFileWorkspace::save(&abs_path, file_state);
378 }
379
380 fn sync_all_terminal_backing_files(&mut self) {
385 use std::io::BufWriter;
386
387 let terminals_to_sync: Vec<_> = self
389 .terminal_buffers
390 .values()
391 .copied()
392 .filter_map(|terminal_id| {
393 self.terminal_backing_files
394 .get(&terminal_id)
395 .map(|path| (terminal_id, path.clone()))
396 })
397 .collect();
398
399 for (terminal_id, backing_path) in terminals_to_sync {
400 if let Some(handle) = self.terminal_manager.get(terminal_id) {
401 if let Ok(state) = handle.state.lock() {
402 if let Ok(mut file) = self.filesystem.open_file_for_append(&backing_path) {
404 let mut writer = BufWriter::new(&mut *file);
405 if let Err(e) = state.append_visible_screen(&mut writer) {
406 tracing::warn!(
407 "Failed to sync terminal {:?} to backing file: {}",
408 terminal_id,
409 e
410 );
411 }
412 }
413 }
414 }
415 }
416 }
417
418 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
422 tracing::debug!("Attempting to restore workspace for {:?}", self.working_dir);
423 match Workspace::load(&self.working_dir)? {
424 Some(workspace) => {
425 tracing::info!("Found workspace, applying...");
426 self.apply_workspace(&workspace)?;
427 Ok(true)
428 }
429 None => {
430 tracing::debug!("No workspace found for {:?}", self.working_dir);
431 Ok(false)
432 }
433 }
434 }
435
436 pub fn apply_workspace(&mut self, workspace: &Workspace) -> Result<(), WorkspaceError> {
438 tracing::debug!(
439 "Applying workspace with {} split states",
440 workspace.split_states.len()
441 );
442
443 if let Some(line_numbers) = workspace.config_overrides.line_numbers {
445 self.config.editor.line_numbers = line_numbers;
446 }
447 if let Some(relative_line_numbers) = workspace.config_overrides.relative_line_numbers {
448 self.config.editor.relative_line_numbers = relative_line_numbers;
449 }
450 if let Some(line_wrap) = workspace.config_overrides.line_wrap {
451 self.config.editor.line_wrap = line_wrap;
452 }
453 if let Some(syntax_highlighting) = workspace.config_overrides.syntax_highlighting {
454 self.config.editor.syntax_highlighting = syntax_highlighting;
455 }
456 if let Some(enable_inlay_hints) = workspace.config_overrides.enable_inlay_hints {
457 self.config.editor.enable_inlay_hints = enable_inlay_hints;
458 }
459 if let Some(mouse_enabled) = workspace.config_overrides.mouse_enabled {
460 self.mouse_enabled = mouse_enabled;
461 }
462 if let Some(menu_bar_hidden) = workspace.config_overrides.menu_bar_hidden {
463 self.menu_bar_visible = !menu_bar_hidden;
464 }
465
466 self.search_case_sensitive = workspace.search_options.case_sensitive;
468 self.search_whole_word = workspace.search_options.whole_word;
469 self.search_use_regex = workspace.search_options.use_regex;
470 self.search_confirm_each = workspace.search_options.confirm_each;
471
472 tracing::debug!(
474 "Restoring histories: {} search, {} replace, {} goto_line",
475 workspace.histories.search.len(),
476 workspace.histories.replace.len(),
477 workspace.histories.goto_line.len()
478 );
479 for item in &workspace.histories.search {
480 self.get_or_create_prompt_history("search")
481 .push(item.clone());
482 }
483 for item in &workspace.histories.replace {
484 self.get_or_create_prompt_history("replace")
485 .push(item.clone());
486 }
487 for item in &workspace.histories.goto_line {
488 self.get_or_create_prompt_history("goto_line")
489 .push(item.clone());
490 }
491
492 self.file_explorer_visible = workspace.file_explorer.visible;
494 self.file_explorer_width_percent = workspace.file_explorer.width_percent;
495
496 if workspace.file_explorer.show_hidden {
499 self.pending_file_explorer_show_hidden = Some(true);
500 }
501 if workspace.file_explorer.show_gitignored {
502 self.pending_file_explorer_show_gitignored = Some(true);
503 }
504
505 if self.file_explorer_visible && self.file_explorer.is_none() {
508 self.init_file_explorer();
509 }
510
511 let file_paths = collect_file_paths_from_states(&workspace.split_states);
514 tracing::debug!(
515 "Workspace has {} files to restore: {:?}",
516 file_paths.len(),
517 file_paths
518 );
519 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
520
521 for rel_path in file_paths {
522 let abs_path = self.working_dir.join(&rel_path);
523 tracing::trace!(
524 "Checking file: {:?} (exists: {})",
525 abs_path,
526 abs_path.exists()
527 );
528 if abs_path.exists() {
529 match self.open_file_internal(&abs_path) {
531 Ok(buffer_id) => {
532 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
533 path_to_buffer.insert(rel_path, buffer_id);
534 }
535 Err(e) => {
536 tracing::warn!("Failed to open file {:?}: {}", abs_path, e);
537 }
538 }
539 } else {
540 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
541 }
542 }
543
544 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
545
546 if !workspace.external_files.is_empty() {
549 tracing::debug!(
550 "Restoring {} external files: {:?}",
551 workspace.external_files.len(),
552 workspace.external_files
553 );
554 for abs_path in &workspace.external_files {
555 if abs_path.exists() {
556 match self.open_file_internal(abs_path) {
557 Ok(buffer_id) => {
558 tracing::debug!(
559 "Restored external file {:?} as buffer {:?}",
560 abs_path,
561 buffer_id
562 );
563 }
564 Err(e) => {
565 tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e);
566 }
567 }
568 } else {
569 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
570 }
571 }
572 }
573
574 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
576 if !workspace.terminals.is_empty() {
577 if let Some(ref bridge) = self.async_bridge {
578 self.terminal_manager.set_async_bridge(bridge.clone());
579 }
580 for terminal in &workspace.terminals {
581 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
582 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
583 }
584 }
585 }
586
587 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
590 self.restore_split_node(
591 &workspace.split_layout,
592 &path_to_buffer,
593 &terminal_buffer_map,
594 &workspace.split_states,
595 &mut split_id_map,
596 true, );
598
599 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
603 self.split_manager
604 .set_active_split(LeafId(new_active_split));
605 }
606
607 for (key, bookmark) in &workspace.bookmarks {
609 if let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) {
610 if let Some(buffer) = self.buffers.get(&buffer_id) {
612 let pos = bookmark.position.min(buffer.buffer.len());
613 self.bookmarks.insert(
614 *key,
615 Bookmark {
616 buffer_id,
617 position: pos,
618 },
619 );
620 }
621 }
622 }
623
624 tracing::debug!(
625 "Workspace restore complete: {} splits, {} buffers",
626 self.split_view_states.len(),
627 self.buffers.len()
628 );
629
630 #[cfg(feature = "plugins")]
635 {
636 let buffer_id = self.active_buffer();
637 self.update_plugin_state_snapshot();
638 tracing::debug!(
639 "Firing buffer_activated for active buffer {:?} after workspace restore",
640 buffer_id
641 );
642 self.plugin_manager.run_hook(
643 "buffer_activated",
644 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
645 );
646 }
647
648 Ok(())
649 }
650
651 fn restore_terminal_from_workspace(
660 &mut self,
661 terminal: &SerializedTerminalWorkspace,
662 ) -> Option<BufferId> {
663 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
665 let log_path = if terminal.log_path.is_absolute() {
666 terminal.log_path.clone()
667 } else {
668 terminals_root.join(&terminal.log_path)
669 };
670 let backing_path = if terminal.backing_path.is_absolute() {
671 terminal.backing_path.clone()
672 } else {
673 terminals_root.join(&terminal.backing_path)
674 };
675
676 #[allow(clippy::let_underscore_must_use)]
678 let _ = self.filesystem.create_dir_all(
679 log_path
680 .parent()
681 .or_else(|| backing_path.parent())
682 .unwrap_or(&terminals_root),
683 );
684
685 let predicted_id = self.terminal_manager.next_terminal_id();
687 self.terminal_log_files
688 .insert(predicted_id, log_path.clone());
689 self.terminal_backing_files
690 .insert(predicted_id, backing_path.clone());
691
692 let terminal_id = match self.terminal_manager.spawn(
694 terminal.cols,
695 terminal.rows,
696 terminal.cwd.clone(),
697 Some(log_path.clone()),
698 Some(backing_path.clone()),
699 ) {
700 Ok(id) => id,
701 Err(e) => {
702 tracing::warn!(
703 "Failed to restore terminal {}: {}",
704 terminal.terminal_index,
705 e
706 );
707 return None;
708 }
709 };
710
711 if terminal_id != predicted_id {
713 self.terminal_log_files
714 .insert(terminal_id, log_path.clone());
715 self.terminal_backing_files
716 .insert(terminal_id, backing_path.clone());
717 self.terminal_log_files.remove(&predicted_id);
718 self.terminal_backing_files.remove(&predicted_id);
719 }
720
721 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
723
724 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
727
728 Some(buffer_id)
729 }
730
731 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
736 if !backing_path.exists() {
738 return;
739 }
740
741 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
742 if let Ok(new_state) = EditorState::from_file_with_languages(
743 backing_path,
744 self.terminal_width,
745 self.terminal_height,
746 large_file_threshold,
747 &self.grammar_registry,
748 &self.config.languages,
749 std::sync::Arc::clone(&self.filesystem),
750 ) {
751 if let Some(state) = self.buffers.get_mut(&buffer_id) {
752 *state = new_state;
753 let total = state.buffer.total_bytes();
755 for vs in self.split_view_states.values_mut() {
757 if vs.open_buffers.contains(&buffer_id) {
758 vs.cursors.primary_mut().position = total;
759 }
760 }
761 state.buffer.set_modified(false);
763 state.editing_disabled = true;
765 state.margins.configure_for_line_numbers(false);
766 }
767 }
768 }
769
770 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
772 for (buffer_id, metadata) in &self.buffer_metadata {
774 if let Some(file_path) = metadata.file_path() {
775 if file_path == path {
776 return Ok(*buffer_id);
777 }
778 }
779 }
780
781 self.open_file(path).map_err(WorkspaceError::Io)
783 }
784
785 fn restore_split_node(
787 &mut self,
788 node: &SerializedSplitNode,
789 path_to_buffer: &HashMap<PathBuf, BufferId>,
790 terminal_buffers: &HashMap<usize, BufferId>,
791 split_states: &HashMap<usize, SerializedSplitViewState>,
792 split_id_map: &mut HashMap<usize, SplitId>,
793 is_first_leaf: bool,
794 ) {
795 match node {
796 SerializedSplitNode::Leaf {
797 file_path,
798 split_id,
799 label,
800 } => {
801 let buffer_id = file_path
803 .as_ref()
804 .and_then(|p| path_to_buffer.get(p).copied())
805 .unwrap_or(self.active_buffer());
806
807 let current_leaf_id = if is_first_leaf {
808 let leaf_id = self.split_manager.active_split();
810 self.split_manager.set_split_buffer(leaf_id, buffer_id);
811 leaf_id
812 } else {
813 self.split_manager.active_split()
815 };
816
817 split_id_map.insert(*split_id, current_leaf_id.into());
819
820 if let Some(label) = label {
822 self.split_manager.set_label(current_leaf_id, label.clone());
823 }
824
825 self.restore_split_view_state(
827 current_leaf_id,
828 *split_id,
829 split_states,
830 path_to_buffer,
831 terminal_buffers,
832 );
833 }
834 SerializedSplitNode::Terminal {
835 terminal_index,
836 split_id,
837 label,
838 } => {
839 let buffer_id = terminal_buffers
840 .get(terminal_index)
841 .copied()
842 .unwrap_or(self.active_buffer());
843
844 let current_leaf_id = if is_first_leaf {
845 let leaf_id = self.split_manager.active_split();
846 self.split_manager.set_split_buffer(leaf_id, buffer_id);
847 leaf_id
848 } else {
849 self.split_manager.active_split()
850 };
851
852 split_id_map.insert(*split_id, current_leaf_id.into());
853
854 if let Some(label) = label {
856 self.split_manager.set_label(current_leaf_id, label.clone());
857 }
858
859 self.split_manager
860 .set_split_buffer(current_leaf_id, buffer_id);
861
862 self.restore_split_view_state(
863 current_leaf_id,
864 *split_id,
865 split_states,
866 path_to_buffer,
867 terminal_buffers,
868 );
869 }
870 SerializedSplitNode::Split {
871 direction,
872 first,
873 second,
874 ratio,
875 split_id,
876 } => {
877 self.restore_split_node(
879 first,
880 path_to_buffer,
881 terminal_buffers,
882 split_states,
883 split_id_map,
884 is_first_leaf,
885 );
886
887 let second_buffer_id =
889 get_first_leaf_buffer(second, path_to_buffer, terminal_buffers)
890 .unwrap_or(self.active_buffer());
891
892 let split_direction = match direction {
894 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
895 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
896 };
897
898 match self
900 .split_manager
901 .split_active(split_direction, second_buffer_id, *ratio)
902 {
903 Ok(new_leaf_id) => {
904 let mut view_state = SplitViewState::with_buffer(
906 self.terminal_width,
907 self.terminal_height,
908 second_buffer_id,
909 );
910 view_state.apply_config_defaults(
911 self.config.editor.line_numbers,
912 self.config.editor.line_wrap,
913 self.config.editor.wrap_indent,
914 self.config.editor.rulers.clone(),
915 );
916 self.split_view_states.insert(new_leaf_id, view_state);
917
918 split_id_map.insert(*split_id, new_leaf_id.into());
920
921 self.restore_split_node(
923 second,
924 path_to_buffer,
925 terminal_buffers,
926 split_states,
927 split_id_map,
928 false,
929 );
930 }
931 Err(e) => {
932 tracing::error!("Failed to create split during workspace restore: {}", e);
933 }
934 }
935 }
936 }
937 }
938
939 fn restore_split_view_state(
941 &mut self,
942 current_split_id: LeafId,
943 saved_split_id: usize,
944 split_states: &HashMap<usize, SerializedSplitViewState>,
945 path_to_buffer: &HashMap<PathBuf, BufferId>,
946 terminal_buffers: &HashMap<usize, BufferId>,
947 ) {
948 let Some(split_state) = split_states.get(&saved_split_id) else {
950 return;
951 };
952
953 let Some(view_state) = self.split_view_states.get_mut(¤t_split_id) else {
954 return;
955 };
956
957 let mut active_buffer_id: Option<BufferId> = None;
958
959 if !split_state.open_tabs.is_empty() {
960 for tab in &split_state.open_tabs {
961 match tab {
962 SerializedTabRef::File(rel_path) => {
963 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
964 if !view_state.open_buffers.contains(&buffer_id) {
965 view_state.open_buffers.push(buffer_id);
966 }
967 view_state.ensure_buffer_state(buffer_id);
969 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
970 view_state
971 .buffer_state_mut(buffer_id)
972 .unwrap()
973 .viewport
974 .line_wrap_enabled = false;
975 }
976 }
977 }
978 SerializedTabRef::Terminal(index) => {
979 if let Some(&buffer_id) = terminal_buffers.get(index) {
980 if !view_state.open_buffers.contains(&buffer_id) {
981 view_state.open_buffers.push(buffer_id);
982 }
983 view_state
984 .ensure_buffer_state(buffer_id)
985 .viewport
986 .line_wrap_enabled = false;
987 }
988 }
989 }
990 }
991
992 if let Some(active_idx) = split_state.active_tab_index {
993 if let Some(tab) = split_state.open_tabs.get(active_idx) {
994 active_buffer_id = match tab {
995 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
996 SerializedTabRef::Terminal(index) => terminal_buffers.get(index).copied(),
997 };
998 }
999 }
1000 } else {
1001 for rel_path in &split_state.open_files {
1003 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1004 if !view_state.open_buffers.contains(&buffer_id) {
1005 view_state.open_buffers.push(buffer_id);
1006 }
1007 view_state.ensure_buffer_state(buffer_id);
1008 }
1009 }
1010
1011 let active_file_path = split_state.open_files.get(split_state.active_file_index);
1012 active_buffer_id =
1013 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1014 }
1015
1016 for (rel_path, file_state) in &split_state.file_states {
1018 let buffer_id = match path_to_buffer.get(rel_path).copied() {
1019 Some(id) => id,
1020 None => continue,
1021 };
1022 let max_pos = self
1023 .buffers
1024 .get(&buffer_id)
1025 .map(|b| b.buffer.len())
1026 .unwrap_or(0);
1027
1028 let buf_state = view_state.ensure_buffer_state(buffer_id);
1030
1031 let cursor_pos = file_state.cursor.position.min(max_pos);
1032 buf_state.cursors.primary_mut().position = cursor_pos;
1033 buf_state.cursors.primary_mut().anchor =
1034 file_state.cursor.anchor.map(|a| a.min(max_pos));
1035 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1036
1037 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1038 buf_state.viewport.top_view_line_offset = file_state.scroll.top_view_line_offset;
1039 buf_state.viewport.left_column = file_state.scroll.left_column;
1040 buf_state.viewport.set_skip_resize_sync();
1041
1042 buf_state.view_mode = match file_state.view_mode {
1044 SerializedViewMode::Source => ViewMode::Source,
1045 SerializedViewMode::Compose => ViewMode::Compose,
1046 };
1047 buf_state.compose_width = file_state.compose_width;
1048 buf_state.plugin_state = file_state.plugin_state.clone();
1049 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1050 buf_state.folds.clear(&mut state.marker_list);
1051 for fold in &file_state.folds {
1052 let start_line = fold.header_line.saturating_add(1);
1053 let end_line = fold.end_line;
1054 if start_line > end_line {
1055 continue;
1056 }
1057 let Some(start_byte) = state.buffer.line_start_offset(start_line) else {
1058 continue;
1059 };
1060 let end_byte = state
1061 .buffer
1062 .line_start_offset(end_line.saturating_add(1))
1063 .unwrap_or_else(|| state.buffer.len());
1064 buf_state.folds.add(
1065 &mut state.marker_list,
1066 start_byte,
1067 end_byte,
1068 fold.placeholder.clone(),
1069 );
1070 }
1071 }
1072
1073 tracing::trace!(
1074 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1075 rel_path,
1076 cursor_pos,
1077 buf_state.viewport.top_byte,
1078 buf_state.view_mode,
1079 );
1080 }
1081
1082 let restored_view_mode = match split_state.view_mode {
1085 SerializedViewMode::Source => ViewMode::Source,
1086 SerializedViewMode::Compose => ViewMode::Compose,
1087 };
1088
1089 if let Some(active_id) = active_buffer_id {
1090 view_state.switch_buffer(active_id);
1092
1093 let active_has_file_state = split_state
1095 .file_states
1096 .keys()
1097 .any(|rel_path| path_to_buffer.get(rel_path).copied() == Some(active_id));
1098 if !active_has_file_state {
1099 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1100 view_state.active_state_mut().compose_width = split_state.compose_width;
1101 }
1102
1103 self.split_manager
1107 .set_split_buffer(current_split_id, active_id);
1108 }
1109 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1110 }
1111}
1112
1113fn get_first_leaf_buffer(
1115 node: &SerializedSplitNode,
1116 path_to_buffer: &HashMap<PathBuf, BufferId>,
1117 terminal_buffers: &HashMap<usize, BufferId>,
1118) -> Option<BufferId> {
1119 match node {
1120 SerializedSplitNode::Leaf { file_path, .. } => file_path
1121 .as_ref()
1122 .and_then(|p| path_to_buffer.get(p).copied()),
1123 SerializedSplitNode::Terminal { terminal_index, .. } => {
1124 terminal_buffers.get(terminal_index).copied()
1125 }
1126 SerializedSplitNode::Split { first, .. } => {
1127 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers)
1128 }
1129 }
1130}
1131
1132fn serialize_split_node(
1137 node: &SplitNode,
1138 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1139 working_dir: &Path,
1140 terminal_buffers: &HashMap<BufferId, TerminalId>,
1141 terminal_indices: &HashMap<TerminalId, usize>,
1142 split_labels: &HashMap<SplitId, String>,
1143) -> SerializedSplitNode {
1144 match node {
1145 SplitNode::Leaf {
1146 buffer_id,
1147 split_id,
1148 } => {
1149 let raw_split_id: SplitId = (*split_id).into();
1150 let label = split_labels.get(&raw_split_id).cloned();
1151
1152 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1153 if let Some(index) = terminal_indices.get(terminal_id) {
1154 return SerializedSplitNode::Terminal {
1155 terminal_index: *index,
1156 split_id: raw_split_id.0,
1157 label,
1158 };
1159 }
1160 }
1161
1162 let file_path = buffer_metadata
1163 .get(buffer_id)
1164 .and_then(|meta| meta.file_path())
1165 .and_then(|abs_path| {
1166 abs_path
1167 .strip_prefix(working_dir)
1168 .ok()
1169 .map(|p| p.to_path_buf())
1170 });
1171
1172 SerializedSplitNode::Leaf {
1173 file_path,
1174 split_id: raw_split_id.0,
1175 label,
1176 }
1177 }
1178 SplitNode::Split {
1179 direction,
1180 first,
1181 second,
1182 ratio,
1183 split_id,
1184 } => {
1185 let raw_split_id: SplitId = (*split_id).into();
1186 SerializedSplitNode::Split {
1187 direction: match direction {
1188 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
1189 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
1190 },
1191 first: Box::new(serialize_split_node(
1192 first,
1193 buffer_metadata,
1194 working_dir,
1195 terminal_buffers,
1196 terminal_indices,
1197 split_labels,
1198 )),
1199 second: Box::new(serialize_split_node(
1200 second,
1201 buffer_metadata,
1202 working_dir,
1203 terminal_buffers,
1204 terminal_indices,
1205 split_labels,
1206 )),
1207 ratio: *ratio,
1208 split_id: raw_split_id.0,
1209 }
1210 }
1211 }
1212}
1213
1214fn serialize_split_view_state(
1215 view_state: &crate::view::split::SplitViewState,
1216 buffers: &HashMap<BufferId, EditorState>,
1217 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1218 working_dir: &Path,
1219 active_buffer: Option<BufferId>,
1220 terminal_buffers: &HashMap<BufferId, TerminalId>,
1221 terminal_indices: &HashMap<TerminalId, usize>,
1222) -> SerializedSplitViewState {
1223 let mut open_tabs = Vec::new();
1224 let mut open_files = Vec::new();
1225 let mut active_tab_index = None;
1226
1227 for buffer_id in &view_state.open_buffers {
1228 let tab_index = open_tabs.len();
1229 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1230 if let Some(idx) = terminal_indices.get(terminal_id) {
1231 open_tabs.push(SerializedTabRef::Terminal(*idx));
1232 if Some(*buffer_id) == active_buffer {
1233 active_tab_index = Some(tab_index);
1234 }
1235 continue;
1236 }
1237 }
1238
1239 if let Some(rel_path) = buffer_metadata
1240 .get(buffer_id)
1241 .and_then(|meta| meta.file_path())
1242 .and_then(|abs_path| abs_path.strip_prefix(working_dir).ok())
1243 {
1244 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
1245 open_files.push(rel_path.to_path_buf());
1246 if Some(*buffer_id) == active_buffer {
1247 active_tab_index = Some(tab_index);
1248 }
1249 }
1250 }
1251
1252 let active_file_index = active_tab_index
1254 .and_then(|idx| open_tabs.get(idx))
1255 .and_then(|tab| match tab {
1256 SerializedTabRef::File(path) => {
1257 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
1258 }
1259 _ => None,
1260 })
1261 .unwrap_or(0);
1262
1263 let mut file_states = HashMap::new();
1265 for (buffer_id, buf_state) in &view_state.keyed_states {
1266 if let Some(meta) = buffer_metadata.get(buffer_id) {
1267 if let Some(abs_path) = meta.file_path() {
1268 if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
1269 let primary_cursor = buf_state.cursors.primary();
1270 let folds = buffers
1271 .get(buffer_id)
1272 .map(|state| {
1273 buf_state
1274 .folds
1275 .collapsed_line_ranges(&state.buffer, &state.marker_list)
1276 .into_iter()
1277 .map(|range| SerializedFoldRange {
1278 header_line: range.header_line,
1279 end_line: range.end_line,
1280 placeholder: range.placeholder,
1281 })
1282 .collect::<Vec<_>>()
1283 })
1284 .unwrap_or_default();
1285
1286 file_states.insert(
1287 rel_path.to_path_buf(),
1288 SerializedFileState {
1289 cursor: SerializedCursor {
1290 position: primary_cursor.position,
1291 anchor: primary_cursor.anchor,
1292 sticky_column: primary_cursor.sticky_column,
1293 },
1294 additional_cursors: buf_state
1295 .cursors
1296 .iter()
1297 .skip(1) .map(|(_, cursor)| SerializedCursor {
1299 position: cursor.position,
1300 anchor: cursor.anchor,
1301 sticky_column: cursor.sticky_column,
1302 })
1303 .collect(),
1304 scroll: SerializedScroll {
1305 top_byte: buf_state.viewport.top_byte,
1306 top_view_line_offset: buf_state.viewport.top_view_line_offset,
1307 left_column: buf_state.viewport.left_column,
1308 },
1309 view_mode: match buf_state.view_mode {
1310 ViewMode::Source => SerializedViewMode::Source,
1311 ViewMode::Compose => SerializedViewMode::Compose,
1312 },
1313 compose_width: buf_state.compose_width,
1314 plugin_state: buf_state.plugin_state.clone(),
1315 folds,
1316 },
1317 );
1318 }
1319 }
1320 }
1321 }
1322
1323 let active_view_mode = active_buffer
1325 .and_then(|id| view_state.keyed_states.get(&id))
1326 .map(|bs| match bs.view_mode {
1327 ViewMode::Source => SerializedViewMode::Source,
1328 ViewMode::Compose => SerializedViewMode::Compose,
1329 })
1330 .unwrap_or(SerializedViewMode::Source);
1331 let active_compose_width = active_buffer
1332 .and_then(|id| view_state.keyed_states.get(&id))
1333 .and_then(|bs| bs.compose_width);
1334
1335 SerializedSplitViewState {
1336 open_tabs,
1337 active_tab_index,
1338 open_files,
1339 active_file_index,
1340 file_states,
1341 tab_scroll_offset: view_state.tab_scroll_offset,
1342 view_mode: active_view_mode,
1343 compose_width: active_compose_width,
1344 }
1345}
1346
1347fn serialize_bookmarks(
1348 bookmarks: &HashMap<char, Bookmark>,
1349 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1350 working_dir: &Path,
1351) -> HashMap<char, SerializedBookmark> {
1352 bookmarks
1353 .iter()
1354 .filter_map(|(key, bookmark)| {
1355 buffer_metadata
1356 .get(&bookmark.buffer_id)
1357 .and_then(|meta| meta.file_path())
1358 .and_then(|abs_path| {
1359 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
1360 (
1361 *key,
1362 SerializedBookmark {
1363 file_path: rel_path.to_path_buf(),
1364 position: bookmark.position,
1365 },
1366 )
1367 })
1368 })
1369 })
1370 .collect()
1371}
1372
1373fn collect_file_paths_from_states(
1375 split_states: &HashMap<usize, SerializedSplitViewState>,
1376) -> Vec<PathBuf> {
1377 let mut paths = Vec::new();
1378 for state in split_states.values() {
1379 if !state.open_tabs.is_empty() {
1380 for tab in &state.open_tabs {
1381 if let SerializedTabRef::File(path) = tab {
1382 if !paths.contains(path) {
1383 paths.push(path.clone());
1384 }
1385 }
1386 }
1387 } else {
1388 for path in &state.open_files {
1389 if !paths.contains(path) {
1390 paths.push(path.clone());
1391 }
1392 }
1393 }
1394 }
1395 paths
1396}
1397
1398fn get_expanded_dirs(
1400 explorer: &crate::view::file_tree::FileTreeView,
1401 working_dir: &Path,
1402) -> Vec<PathBuf> {
1403 let mut expanded = Vec::new();
1404 let tree = explorer.tree();
1405
1406 for node in tree.all_nodes() {
1408 if node.is_expanded() && node.is_dir() {
1409 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
1411 expanded.push(rel_path.to_path_buf());
1412 }
1413 }
1414 }
1415
1416 expanded
1417}