1use std::collections::{HashMap, HashSet};
26use std::path::{Path, PathBuf};
27use std::time::Instant;
28
29use crate::state::EditorState;
30
31use crate::model::event::{BufferId, LeafId, SplitDirection, SplitId};
32use crate::services::terminal::TerminalId;
33use crate::state::ViewMode;
34use crate::view::split::{SplitNode, SplitViewState};
35use crate::workspace::{
36 FileExplorerState, PersistedFileWorkspace, SearchOptions, SerializedBookmark, SerializedCursor,
37 SerializedFileState, SerializedFoldRange, SerializedScroll, SerializedSplitDirection,
38 SerializedSplitNode, SerializedSplitViewState, SerializedTabRef, SerializedTerminalWorkspace,
39 SerializedViewMode, UnnamedBufferRef, Workspace, WorkspaceConfigOverrides, WorkspaceError,
40 WorkspaceHistories, WORKSPACE_VERSION,
41};
42
43use super::bookmarks::{Bookmark, BookmarkState};
44use super::Editor;
45
46fn resolve_fold_header_line(
58 buffer: &crate::model::buffer::Buffer,
59 saved_line: usize,
60 header_text: Option<&str>,
61) -> Option<usize> {
62 let Some(expected) = header_text else {
63 return Some(saved_line);
65 };
66 let expected_trimmed = expected.trim();
67 let line_matches = |line: usize| -> bool {
68 buffer
69 .get_line(line)
70 .map(|bytes| {
71 let text = String::from_utf8_lossy(&bytes);
72 text.trim_end_matches('\n').trim_end_matches('\r').trim() == expected_trimmed
73 })
74 .unwrap_or(false)
75 };
76 if line_matches(saved_line) {
77 return Some(saved_line);
78 }
79 const SEARCH_WINDOW: usize = 32;
81 for delta in 1..=SEARCH_WINDOW {
82 let above = saved_line.checked_sub(delta);
83 if let Some(l) = above {
84 if line_matches(l) {
85 return Some(l);
86 }
87 }
88 let below = saved_line.saturating_add(delta);
89 if line_matches(below) {
90 return Some(below);
91 }
92 }
93 None
94}
95
96pub struct WorkspaceTracker {
100 dirty: bool,
102 last_save: Instant,
104 save_interval: std::time::Duration,
106 enabled: bool,
108}
109
110impl WorkspaceTracker {
111 pub fn new(enabled: bool) -> Self {
113 Self {
114 dirty: false,
115 last_save: Instant::now(),
116 save_interval: std::time::Duration::from_secs(5),
117 enabled,
118 }
119 }
120
121 pub fn is_enabled(&self) -> bool {
123 self.enabled
124 }
125
126 pub fn mark_dirty(&mut self) {
128 if self.enabled {
129 self.dirty = true;
130 }
131 }
132
133 pub fn should_save(&self) -> bool {
135 self.enabled && self.dirty && self.last_save.elapsed() >= self.save_interval
136 }
137
138 pub fn record_save(&mut self) {
140 self.dirty = false;
141 self.last_save = Instant::now();
142 }
143
144 pub fn is_dirty(&self) -> bool {
146 self.dirty
147 }
148}
149
150impl Editor {
151 pub fn capture_workspace(&self) -> Workspace {
153 tracing::debug!("Capturing workspace for {:?}", self.working_dir);
154
155 let mut terminals = Vec::new();
157 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
158 let mut seen = HashSet::new();
159 for terminal_id in self.active_window().terminal_buffers.values().copied() {
160 if seen.insert(terminal_id) {
161 if self
168 .active_window()
169 .ephemeral_terminals
170 .contains(&terminal_id)
171 {
172 continue;
173 }
174 let idx = terminals.len();
175 terminal_indices.insert(terminal_id, idx);
176 let handle = self.active_window().terminal_manager.get(terminal_id);
177 let (cols, rows) = handle
178 .map(|h| h.size())
179 .unwrap_or((self.terminal_width, self.terminal_height));
180 let cwd = handle.and_then(|h| h.cwd());
181 let shell = handle
182 .map(|h| h.shell().to_string())
183 .unwrap_or_else(crate::services::terminal::detect_shell);
184 let log_path = self
185 .active_window()
186 .terminal_log_files
187 .get(&terminal_id)
188 .cloned()
189 .unwrap_or_else(|| {
190 let root = self.dir_context.terminal_dir_for(&self.working_dir);
191 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
192 });
193 let backing_path = self
194 .active_window()
195 .terminal_backing_files
196 .get(&terminal_id)
197 .cloned()
198 .unwrap_or_else(|| {
199 let root = self.dir_context.terminal_dir_for(&self.working_dir);
200 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
201 });
202
203 terminals.push(SerializedTerminalWorkspace {
204 terminal_index: idx,
205 cwd,
206 shell,
207 cols,
208 rows,
209 log_path,
210 backing_path,
211 });
212 }
213 }
214
215 let split_layout = serialize_split_node(
216 self.windows
217 .get(&self.active_window)
218 .and_then(|w| w.buffers.splits())
219 .map(|(mgr, _)| mgr)
220 .expect("active window must have a populated split layout")
221 .root(),
222 &self.active_window().buffer_metadata,
223 &self.working_dir,
224 self.windows
225 .get(&self.active_window)
226 .map(|w| &w.terminal_buffers)
227 .expect("active window present"),
228 &terminal_indices,
229 self.windows
230 .get(&self.active_window)
231 .and_then(|w| w.buffers.splits())
232 .map(|(mgr, _)| mgr)
233 .expect("active window must have a populated split layout")
234 .labels(),
235 );
236
237 let active_buffers: HashMap<LeafId, BufferId> = self
240 .windows
241 .get(&self.active_window)
242 .and_then(|w| w.buffers.splits())
243 .map(|(mgr, _)| mgr)
244 .expect("active window must have a populated split layout")
245 .root()
246 .get_leaves_with_rects(ratatui::layout::Rect::default())
247 .into_iter()
248 .map(|(leaf_id, buffer_id, _)| (leaf_id, buffer_id))
249 .collect();
250
251 let mut split_states = HashMap::new();
252 for (leaf_id, view_state) in self
253 .windows
254 .get(&self.active_window)
255 .and_then(|w| w.buffers.splits())
256 .map(|(_, vs)| vs)
257 .expect("active window must have a populated split layout")
258 {
259 let active_buffer = active_buffers.get(leaf_id).copied();
260 let serialized = serialize_split_view_state(
261 view_state,
262 self.windows
263 .get(&self.active_window)
264 .map(|w| w.buffers.as_map())
265 .expect("active window present"),
266 &self.active_window().buffer_metadata,
267 &self.working_dir,
268 active_buffer,
269 self.windows
270 .get(&self.active_window)
271 .map(|w| &w.terminal_buffers)
272 .expect("active window present"),
273 &terminal_indices,
274 );
275 tracing::trace!(
276 "Split {:?}: {} open tabs, active_buffer={:?}",
277 leaf_id,
278 serialized.open_tabs.len(),
279 active_buffer
280 );
281 split_states.insert(leaf_id.0 .0, serialized);
282 }
283
284 tracing::debug!(
285 "Captured {} split states, active_split={}",
286 split_states.len(),
287 SplitId::from(
288 self.windows
289 .get(&self.active_window)
290 .and_then(|w| w.buffers.splits())
291 .map(|(mgr, _)| mgr)
292 .expect("active window must have a populated split layout")
293 .active_split()
294 )
295 .0
296 );
297
298 let file_explorer = if let Some(explorer) = self.file_explorer().as_ref() {
300 let expanded_dirs = get_expanded_dirs(explorer, &self.working_dir);
302 FileExplorerState {
303 visible: self.file_explorer_visible(),
304 width: self.active_window().file_explorer_width,
305 side: self.active_window().file_explorer_side,
306 expanded_dirs,
307 scroll_offset: explorer.get_scroll_offset(),
308 show_hidden: explorer.ignore_patterns().show_hidden(),
309 show_gitignored: explorer.ignore_patterns().show_gitignored(),
310 }
311 } else {
312 FileExplorerState {
313 visible: self.file_explorer_visible(),
314 width: self.active_window().file_explorer_width,
315 side: self.active_window().file_explorer_side,
316 expanded_dirs: Vec::new(),
317 scroll_offset: 0,
318 show_hidden: false,
319 show_gitignored: false,
320 }
321 };
322
323 let config_overrides = WorkspaceConfigOverrides {
328 line_numbers: Some(self.config.editor.line_numbers),
329 relative_line_numbers: Some(self.config.editor.relative_line_numbers),
330 line_wrap: Some(self.config.editor.line_wrap),
331 syntax_highlighting: Some(self.config.editor.syntax_highlighting),
332 enable_inlay_hints: Some(self.config.editor.enable_inlay_hints),
333 mouse_enabled: Some(self.active_window().mouse_enabled),
334 menu_bar_hidden: None,
335 };
336
337 let histories = WorkspaceHistories {
339 search: self
340 .active_window()
341 .prompt_histories
342 .get("search")
343 .map(|h| h.items().to_vec())
344 .unwrap_or_default(),
345 replace: self
346 .active_window()
347 .prompt_histories
348 .get("replace")
349 .map(|h| h.items().to_vec())
350 .unwrap_or_default(),
351 command_palette: Vec::new(), goto_line: self
353 .active_window()
354 .prompt_histories
355 .get("goto_line")
356 .map(|h| h.items().to_vec())
357 .unwrap_or_default(),
358 open_file: Vec::new(), };
360 tracing::trace!(
361 "Captured histories: {} search, {} replace",
362 histories.search.len(),
363 histories.replace.len()
364 );
365
366 let search_options = SearchOptions {
368 case_sensitive: self.active_window().search_case_sensitive,
369 whole_word: self.active_window().search_whole_word,
370 use_regex: self.active_window().search_use_regex,
371 confirm_each: self.active_window().search_confirm_each,
372 };
373
374 let bookmarks = serialize_bookmarks(
376 &self.active_window().bookmarks,
377 &self.active_window().buffer_metadata,
378 &self.working_dir,
379 );
380
381 let external_files: Vec<PathBuf> = self
384 .active_window()
385 .buffer_metadata
386 .values()
387 .filter_map(|meta| meta.file_path())
388 .filter(|abs_path| abs_path.strip_prefix(&self.working_dir).is_err())
389 .cloned()
390 .collect();
391 if !external_files.is_empty() {
392 tracing::debug!("Captured {} external files", external_files.len());
393 }
394
395 let read_only_files: Vec<PathBuf> = self
399 .active_window()
400 .buffer_metadata
401 .values()
402 .filter(|meta| meta.read_only)
403 .filter_map(|meta| meta.file_path().cloned())
404 .filter(|p| !p.as_os_str().is_empty())
405 .map(|p| {
406 p.strip_prefix(&self.working_dir)
407 .map(|rel| rel.to_path_buf())
408 .unwrap_or(p)
409 })
410 .collect();
411
412 let unnamed_buffers: Vec<UnnamedBufferRef> = if self.config.editor.hot_exit {
414 self.active_window()
415 .buffer_metadata
416 .iter()
417 .filter_map(|(buffer_id, meta)| {
418 let path = meta.file_path()?;
420 if !path.as_os_str().is_empty() {
421 return None;
422 }
423 if meta.hidden_from_tabs || meta.is_virtual() {
425 return None;
426 }
427 let state = self
429 .windows
430 .get(&self.active_window)
431 .map(|w| &w.buffers)
432 .expect("active window present")
433 .get(buffer_id)?;
434 if state.buffer.total_bytes() == 0 {
435 return None;
436 }
437 let recovery_id = meta.recovery_id.clone()?;
439 Some(UnnamedBufferRef {
440 recovery_id,
441 display_name: meta.display_name.clone(),
442 })
443 })
444 .collect()
445 } else {
446 Vec::new()
447 };
448 if !unnamed_buffers.is_empty() {
449 tracing::debug!("Captured {} unnamed buffers", unnamed_buffers.len());
450 }
451
452 Workspace {
453 version: WORKSPACE_VERSION,
454 working_dir: self.working_dir.clone(),
455 split_layout,
456 active_split_id: SplitId::from(
457 self.windows
458 .get(&self.active_window)
459 .and_then(|w| w.buffers.splits())
460 .map(|(mgr, _)| mgr)
461 .expect("active window must have a populated split layout")
462 .active_split(),
463 )
464 .0,
465 split_states,
466 config_overrides,
467 file_explorer,
468 histories,
469 search_options,
470 bookmarks,
471 terminals,
472 external_files,
473 read_only_files,
474 unnamed_buffers,
475 plugin_global_state: self.plugin_global_state.clone(),
476 saved_at: std::time::SystemTime::now()
477 .duration_since(std::time::UNIX_EPOCH)
478 .unwrap_or_default()
479 .as_secs(),
480 }
481 }
482
483 pub fn save_workspace(&mut self) -> Result<(), WorkspaceError> {
489 self.sync_all_terminal_backing_files();
491
492 self.save_all_global_file_states();
494
495 let workspace = self.capture_workspace();
496
497 if let Some(ref session_name) = self.session_name {
499 workspace.save_session(session_name)
500 } else {
501 workspace.save()
502 }
503 }
504
505 fn save_all_global_file_states(&self) {
507 for (leaf_id, view_state) in self
509 .windows
510 .get(&self.active_window)
511 .and_then(|w| w.buffers.splits())
512 .map(|(_, vs)| vs)
513 .expect("active window must have a populated split layout")
514 {
515 let active_buffer = self
517 .windows
518 .get(&self.active_window)
519 .and_then(|w| w.buffers.splits())
520 .map(|(mgr, _)| mgr)
521 .expect("active window must have a populated split layout")
522 .root()
523 .get_leaves_with_rects(ratatui::layout::Rect::default())
524 .into_iter()
525 .find(|(sid, _, _)| *sid == *leaf_id)
526 .map(|(_, buffer_id, _)| buffer_id);
527
528 if let Some(buffer_id) = active_buffer {
529 self.save_buffer_file_state(buffer_id, view_state);
530 }
531 }
532 }
533
534 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
536 let abs_path = match self.active_window().buffer_metadata.get(&buffer_id) {
538 Some(metadata) => match metadata.file_path() {
539 Some(path) => path.to_path_buf(),
540 None => return, },
542 None => return,
543 };
544
545 let primary_cursor = view_state.cursors.primary();
547 let file_state = SerializedFileState {
548 cursor: SerializedCursor {
549 position: primary_cursor.position,
550 anchor: primary_cursor.anchor,
551 sticky_column: primary_cursor.sticky_column,
552 },
553 additional_cursors: view_state
554 .cursors
555 .iter()
556 .skip(1)
557 .map(|(_, cursor)| SerializedCursor {
558 position: cursor.position,
559 anchor: cursor.anchor,
560 sticky_column: cursor.sticky_column,
561 })
562 .collect(),
563 scroll: SerializedScroll {
564 top_byte: view_state.viewport.top_byte,
565 top_view_line_offset: view_state.viewport.top_view_line_offset,
566 left_column: view_state.viewport.left_column,
567 },
568 view_mode: Default::default(),
569 compose_width: None,
570 plugin_state: std::collections::HashMap::new(),
571 folds: Vec::new(),
572 };
573
574 PersistedFileWorkspace::save(&abs_path, file_state);
576 }
577
578 fn sync_all_terminal_backing_files(&mut self) {
583 use std::io::BufWriter;
584
585 let terminals_to_sync: Vec<_> = self
587 .active_window()
588 .terminal_buffers
589 .values()
590 .copied()
591 .filter_map(|terminal_id| {
592 self.active_window()
593 .terminal_backing_files
594 .get(&terminal_id)
595 .map(|path| (terminal_id, path.clone()))
596 })
597 .collect();
598
599 for (terminal_id, backing_path) in terminals_to_sync {
600 if let Some(handle) = self.active_window().terminal_manager.get(terminal_id) {
601 if let Ok(state) = handle.state.lock() {
602 if let Ok(mut file) = self
604 .authority
605 .filesystem
606 .open_file_for_append(&backing_path)
607 {
608 let mut writer = BufWriter::new(&mut *file);
609 if let Err(e) = state.append_visible_screen(&mut writer) {
610 tracing::warn!(
611 "Failed to sync terminal {:?} to backing file: {}",
612 terminal_id,
613 e
614 );
615 }
616 }
617 }
618 }
619 }
620 }
621
622 pub fn try_restore_workspace(&mut self) -> Result<bool, WorkspaceError> {
626 tracing::debug!("Attempting to restore workspace for {:?}", self.working_dir);
627
628 let workspace = if let Some(ref session_name) = self.session_name {
630 Workspace::load_session(session_name, &self.working_dir)?
631 } else {
632 Workspace::load(&self.working_dir)?
633 };
634
635 match workspace {
636 Some(workspace) => {
637 tracing::info!("Found workspace, applying...");
638 self.apply_workspace(&workspace)?;
639 Ok(true)
640 }
641 None => {
642 tracing::debug!("No workspace found for {:?}", self.working_dir);
643 Ok(false)
644 }
645 }
646 }
647
648 pub fn apply_hot_exit_recovery(&mut self) -> anyhow::Result<usize> {
654 if !self.config.editor.hot_exit {
655 return Ok(0);
656 }
657
658 let entries = self.recovery_service.list_recoverable()?;
659 if entries.is_empty() {
660 return Ok(0);
661 }
662
663 let buffer_files: Vec<_> = self
665 .buffers()
666 .iter()
667 .filter_map(|(buffer_id, state)| {
668 let path = state.buffer.file_path()?.to_path_buf();
669 if path.as_os_str().is_empty() {
670 return None; }
672 Some((*buffer_id, path))
673 })
674 .collect();
675
676 let mut recovered = 0;
677 for (buffer_id, file_path) in buffer_files {
678 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
679 let entry = entries.iter().find(|e| e.id == recovery_id);
680 if let Some(entry) = entry {
681 match self.recovery_service.load_recovery(entry) {
682 Ok(crate::services::recovery::RecoveryResult::Recovered {
683 content, ..
684 }) => {
685 let mut mutated = false;
686 if let Some(state) = self
687 .windows
688 .get_mut(&self.active_window)
689 .map(|w| &mut w.buffers)
690 .expect("active window present")
691 .get_mut(&buffer_id)
692 {
693 let current_len = state.buffer.total_bytes();
694 let text = String::from_utf8_lossy(&content).into_owned();
695 let current = state.buffer.get_text_range_mut(0, current_len).ok();
696 let current_text = current
697 .as_ref()
698 .map(|b| String::from_utf8_lossy(b).into_owned());
699 if current_text.as_deref() != Some(&text) {
700 state.buffer.delete(0..current_len);
701 state.buffer.insert(0, &text);
702 state.buffer.set_modified(true);
703 state.buffer.set_recovery_pending(false);
704 if let Some(log) =
707 self.active_window_mut().event_logs.get_mut(&buffer_id)
708 {
709 log.clear_saved_position();
710 }
711 mutated = true;
712 recovered += 1;
713 tracing::info!(
714 "Restored unsaved changes for {:?} from hot exit recovery",
715 file_path
716 );
717 }
718 }
719 if mutated {
720 self.sync_lsp_after_recovery_replay(buffer_id);
721 }
722 }
723 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
724 chunks,
725 ..
726 }) => {
727 let mut mutated = false;
728 if let Some(state) = self
729 .windows
730 .get_mut(&self.active_window)
731 .map(|w| &mut w.buffers)
732 .expect("active window present")
733 .get_mut(&buffer_id)
734 {
735 for chunk in chunks.into_iter().rev() {
736 let text = String::from_utf8_lossy(&chunk.content).into_owned();
737 if chunk.original_len > 0 {
738 state
739 .buffer
740 .delete(chunk.offset..chunk.offset + chunk.original_len);
741 }
742 state.buffer.insert(chunk.offset, &text);
743 }
744 state.buffer.set_modified(true);
745 state.buffer.set_recovery_pending(false);
746 if let Some(log) =
749 self.active_window_mut().event_logs.get_mut(&buffer_id)
750 {
751 log.clear_saved_position();
752 }
753 mutated = true;
754 recovered += 1;
755 tracing::info!(
756 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
757 file_path
758 );
759 }
760 if mutated {
761 self.sync_lsp_after_recovery_replay(buffer_id);
762 }
763 }
764 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
765 original_path,
766 ..
767 }) => {
768 let name = original_path
769 .file_name()
770 .unwrap_or_default()
771 .to_string_lossy();
772 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
773 self.set_status_message(format!(
774 "{} changed on disk; unsaved changes not restored",
775 name
776 ));
777 }
778 Ok(_) => {} Err(e) => {
780 tracing::debug!(
781 "Failed to load hot exit recovery for {:?}: {}",
782 file_path,
783 e
784 );
785 }
786 }
787 }
788 }
789
790 Ok(recovered)
791 }
792
793 pub fn apply_workspace(&mut self, workspace: &Workspace) -> Result<(), WorkspaceError> {
795 tracing::debug!(
796 "Applying workspace with {} split states",
797 workspace.split_states.len()
798 );
799
800 self.restore_config_overrides(&workspace.config_overrides);
801
802 if !workspace.plugin_global_state.is_empty() {
803 tracing::debug!(
804 "Restoring plugin global state for {} plugins",
805 workspace.plugin_global_state.len()
806 );
807 self.plugin_global_state = workspace.plugin_global_state.clone();
808 }
809
810 self.restore_search_options(&workspace.search_options);
811 self.restore_prompt_histories(&workspace.histories);
812 self.restore_file_explorer_settings(&workspace.file_explorer);
813
814 let mut path_to_buffer = self.open_workspace_files(&workspace.split_states);
815 self.restore_external_files(&workspace.external_files, &mut path_to_buffer);
816 self.apply_read_only_flags(&workspace.read_only_files, &path_to_buffer);
817 self.restore_hot_exit_changes(&path_to_buffer);
818
819 let unnamed_buffer_map = self.restore_unnamed_buffers(&workspace.unnamed_buffers);
820 let terminal_buffer_map = self.restore_terminals_from_workspace(&workspace.terminals);
821
822 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
823 self.restore_split_node(
824 &workspace.split_layout,
825 &path_to_buffer,
826 &terminal_buffer_map,
827 &unnamed_buffer_map,
828 &workspace.split_states,
829 &mut split_id_map,
830 true,
831 );
832
833 if let Some(&new_active_split) = split_id_map.get(&workspace.active_split_id) {
834 self.windows
835 .get_mut(&self.active_window)
836 .and_then(|w| w.split_manager_mut())
837 .expect("active window must have a populated split layout")
838 .set_active_split(LeafId(new_active_split));
839 }
840
841 self.restore_bookmarks_from_workspace(&workspace.bookmarks, &path_to_buffer);
842 self.clean_orphaned_buffers();
843 self.log_restore_summary();
844
845 #[cfg(feature = "plugins")]
846 {
847 let buffer_id = self.active_buffer();
848 self.update_plugin_state_snapshot();
849 tracing::debug!(
850 "Firing buffer_activated for active buffer {:?} after workspace restore",
851 buffer_id
852 );
853 self.plugin_manager.read().unwrap().run_hook(
854 "buffer_activated",
855 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
856 );
857 }
858
859 Ok(())
860 }
861
862 fn restore_config_overrides(&mut self, overrides: &WorkspaceConfigOverrides) {
863 if let Some(line_numbers) = overrides.line_numbers {
864 self.config_mut().editor.line_numbers = line_numbers;
865 }
866 if let Some(relative_line_numbers) = overrides.relative_line_numbers {
867 self.config_mut().editor.relative_line_numbers = relative_line_numbers;
868 }
869 if let Some(line_wrap) = overrides.line_wrap {
870 self.config_mut().editor.line_wrap = line_wrap;
871 }
872 if let Some(syntax_highlighting) = overrides.syntax_highlighting {
873 self.config_mut().editor.syntax_highlighting = syntax_highlighting;
874 }
875 if let Some(enable_inlay_hints) = overrides.enable_inlay_hints {
876 self.config_mut().editor.enable_inlay_hints = enable_inlay_hints;
877 }
878 if let Some(mouse_enabled) = overrides.mouse_enabled {
879 self.active_window_mut().mouse_enabled = mouse_enabled;
880 }
881 }
886
887 fn restore_search_options(&mut self, opts: &SearchOptions) {
888 self.active_window_mut().search_case_sensitive = opts.case_sensitive;
889 self.active_window_mut().search_whole_word = opts.whole_word;
890 self.active_window_mut().search_use_regex = opts.use_regex;
891 self.active_window_mut().search_confirm_each = opts.confirm_each;
892 }
893
894 fn restore_prompt_histories(&mut self, histories: &WorkspaceHistories) {
895 tracing::debug!(
896 "Restoring histories: {} search, {} replace, {} goto_line",
897 histories.search.len(),
898 histories.replace.len(),
899 histories.goto_line.len()
900 );
901 for item in &histories.search {
902 self.get_or_create_prompt_history("search")
903 .push(item.clone());
904 }
905 for item in &histories.replace {
906 self.get_or_create_prompt_history("replace")
907 .push(item.clone());
908 }
909 for item in &histories.goto_line {
910 self.get_or_create_prompt_history("goto_line")
911 .push(item.clone());
912 }
913 }
914
915 fn restore_file_explorer_settings(&mut self, fe: &FileExplorerState) {
916 self.active_window_mut().file_explorer_visible = fe.visible;
917 self.active_window_mut().file_explorer_width = fe.width;
918 self.active_window_mut().file_explorer_side = fe.side;
919
920 if fe.show_hidden {
922 self.active_window_mut().pending_file_explorer_show_hidden = Some(true);
923 }
924 if fe.show_gitignored {
925 self.active_window_mut()
926 .pending_file_explorer_show_gitignored = Some(true);
927 }
928
929 if self.file_explorer_visible() && self.file_explorer().is_none() {
931 self.init_file_explorer();
932 }
933 }
934
935 fn open_workspace_files(
938 &mut self,
939 split_states: &HashMap<usize, SerializedSplitViewState>,
940 ) -> HashMap<PathBuf, BufferId> {
941 let file_paths = collect_file_paths_from_states(split_states);
942 tracing::debug!(
943 "Workspace has {} files to restore: {:?}",
944 file_paths.len(),
945 file_paths
946 );
947 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
948 for rel_path in file_paths {
949 let abs_path = self.working_dir.join(&rel_path);
950 tracing::trace!(
951 "Checking file: {:?} (exists: {})",
952 abs_path,
953 abs_path.exists()
954 );
955 if abs_path.exists() {
956 match self.open_file_internal(&abs_path) {
957 Ok(buffer_id) => {
958 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
959 path_to_buffer.insert(rel_path, buffer_id);
960 }
961 Err(e) => tracing::warn!("Failed to open file {:?}: {}", abs_path, e),
962 }
963 } else {
964 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
965 }
966 }
967 tracing::debug!("Opened {} files from workspace", path_to_buffer.len());
968 path_to_buffer
969 }
970
971 fn restore_external_files(
973 &mut self,
974 external_files: &[PathBuf],
975 path_to_buffer: &mut HashMap<PathBuf, BufferId>,
976 ) {
977 if external_files.is_empty() {
978 return;
979 }
980 tracing::debug!(
981 "Restoring {} external files: {:?}",
982 external_files.len(),
983 external_files
984 );
985 for abs_path in external_files {
986 if !abs_path.exists() {
987 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
988 continue;
989 }
990 match self.open_file_internal(abs_path) {
991 Ok(buffer_id) => {
992 path_to_buffer.insert(abs_path.clone(), buffer_id);
993 tracing::debug!(
994 "Restored external file {:?} as buffer {:?}",
995 abs_path,
996 buffer_id
997 );
998 }
999 Err(e) => tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e),
1000 }
1001 }
1002 }
1003
1004 fn apply_read_only_flags(
1007 &mut self,
1008 read_only_files: &[PathBuf],
1009 path_to_buffer: &HashMap<PathBuf, BufferId>,
1010 ) {
1011 for ro_path in read_only_files {
1012 let buffer_id = path_to_buffer
1013 .get(ro_path)
1014 .copied()
1015 .or_else(|| path_to_buffer.get(&self.working_dir.join(ro_path)).copied());
1016 if let Some(id) = buffer_id {
1017 self.active_window_mut().mark_buffer_read_only(id, true);
1018 }
1019 }
1020 }
1021
1022 fn restore_hot_exit_changes(&mut self, path_to_buffer: &HashMap<PathBuf, BufferId>) {
1025 if !self.config.editor.hot_exit {
1026 return;
1027 }
1028 let entries = self.recovery_service.list_recoverable().unwrap_or_default();
1029 if entries.is_empty() {
1030 return;
1031 }
1032 let buffer_ids: Vec<BufferId> = path_to_buffer.values().copied().collect();
1033 for buffer_id in buffer_ids {
1034 let file_path = self
1035 .buffers()
1036 .get(&buffer_id)
1037 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()));
1038 let Some(file_path) = file_path else { continue };
1039
1040 let recovery_id = self.recovery_service.get_buffer_id(Some(&file_path));
1041 let Some(entry) = entries.iter().find(|e| e.id == recovery_id) else {
1042 continue;
1043 };
1044 match self.recovery_service.load_recovery(entry) {
1045 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1046 let mut mutated = false;
1047 if let Some(state) = self
1048 .windows
1049 .get_mut(&self.active_window)
1050 .map(|w| &mut w.buffers)
1051 .expect("active window present")
1052 .get_mut(&buffer_id)
1053 {
1054 let current_len = state.buffer.total_bytes();
1055 let text = String::from_utf8_lossy(&content).into_owned();
1056 let current = state.buffer.get_text_range_mut(0, current_len).ok();
1057 let current_text = current
1058 .as_ref()
1059 .map(|b| String::from_utf8_lossy(b).into_owned());
1060 if current_text.as_deref() != Some(&text) {
1061 state.buffer.delete(0..current_len);
1062 state.buffer.insert(0, &text);
1063 state.buffer.set_modified(true);
1064 state.buffer.set_recovery_pending(false);
1065 mutated = true;
1066 tracing::info!(
1067 "Restored unsaved changes for {:?} from hot exit recovery",
1068 file_path
1069 );
1070 }
1071 }
1072 if let Some(log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1073 log.clear_saved_position();
1074 }
1075 if mutated {
1076 self.sync_lsp_after_recovery_replay(buffer_id);
1077 }
1078 }
1079 Ok(crate::services::recovery::RecoveryResult::RecoveredChunks {
1080 chunks, ..
1081 }) => {
1082 let mut mutated = false;
1083 if let Some(state) = self
1084 .windows
1085 .get_mut(&self.active_window)
1086 .map(|w| &mut w.buffers)
1087 .expect("active window present")
1088 .get_mut(&buffer_id)
1089 {
1090 for chunk in chunks.into_iter().rev() {
1091 let text = String::from_utf8_lossy(&chunk.content).into_owned();
1092 if chunk.original_len > 0 {
1093 state
1094 .buffer
1095 .delete(chunk.offset..chunk.offset + chunk.original_len);
1096 }
1097 state.buffer.insert(chunk.offset, &text);
1098 }
1099 state.buffer.set_modified(true);
1100 state.buffer.set_recovery_pending(false);
1101 mutated = true;
1102 tracing::info!(
1103 "Restored unsaved changes (chunked) for {:?} from hot exit recovery",
1104 file_path
1105 );
1106 }
1107 if let Some(log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1108 log.clear_saved_position();
1109 }
1110 if mutated {
1111 self.sync_lsp_after_recovery_replay(buffer_id);
1112 }
1113 }
1114 Ok(crate::services::recovery::RecoveryResult::OriginalFileModified {
1115 original_path,
1116 ..
1117 }) => {
1118 let name = original_path
1119 .file_name()
1120 .unwrap_or_default()
1121 .to_string_lossy();
1122 tracing::warn!("{} changed on disk; unsaved changes not restored", name);
1123 self.set_status_message(format!(
1124 "{} changed on disk; unsaved changes not restored",
1125 name
1126 ));
1127 }
1128 Ok(_) => {} Err(e) => {
1130 tracing::debug!(
1131 "Failed to load hot exit recovery for {:?}: {}",
1132 file_path,
1133 e
1134 );
1135 }
1136 }
1137 }
1138 }
1139
1140 fn restore_unnamed_buffers(
1143 &mut self,
1144 unnamed_buffers: &[UnnamedBufferRef],
1145 ) -> HashMap<String, BufferId> {
1146 let mut unnamed_buffer_map: HashMap<String, BufferId> = HashMap::new();
1147 if !self.config.editor.hot_exit || unnamed_buffers.is_empty() {
1148 return unnamed_buffer_map;
1149 }
1150 tracing::debug!(
1151 "Restoring {} unnamed buffers from recovery",
1152 unnamed_buffers.len()
1153 );
1154 for unnamed_ref in unnamed_buffers {
1155 let entries = match self.recovery_service.list_recoverable() {
1156 Ok(e) => e,
1157 Err(e) => {
1158 tracing::warn!("Failed to list recovery entries: {}", e);
1159 continue;
1160 }
1161 };
1162 let Some(entry) = entries.iter().find(|e| e.id == unnamed_ref.recovery_id) else {
1163 tracing::debug!(
1164 "Recovery file not found for unnamed buffer {}",
1165 unnamed_ref.recovery_id
1166 );
1167 continue;
1168 };
1169 match self.recovery_service.load_recovery(entry) {
1170 Ok(crate::services::recovery::RecoveryResult::Recovered { content, .. }) => {
1171 let text = String::from_utf8_lossy(&content).into_owned();
1172 let buffer_id = self.new_buffer();
1173 {
1174 let state = self.active_state_mut();
1175 state.buffer.insert(0, &text);
1176 state.buffer.set_modified(true);
1177 state.buffer.set_recovery_pending(false);
1178 }
1179 self.active_event_log_mut().clear_saved_position();
1180 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&buffer_id)
1181 {
1182 meta.recovery_id = Some(unnamed_ref.recovery_id.clone());
1183 meta.display_name = unnamed_ref.display_name.clone();
1184 }
1185 unnamed_buffer_map.insert(unnamed_ref.recovery_id.clone(), buffer_id);
1186 tracing::info!(
1187 "Restored unnamed buffer '{}' (recovery_id={})",
1188 unnamed_ref.display_name,
1189 unnamed_ref.recovery_id
1190 );
1191 }
1192 Ok(other) => {
1193 tracing::warn!(
1194 "Unexpected recovery result for unnamed buffer {}: {:?}",
1195 unnamed_ref.recovery_id,
1196 std::mem::discriminant(&other)
1197 );
1198 }
1199 Err(e) => {
1200 tracing::warn!(
1201 "Failed to load recovery for unnamed buffer {}: {}",
1202 unnamed_ref.recovery_id,
1203 e
1204 );
1205 }
1206 }
1207 }
1208 unnamed_buffer_map
1209 }
1210
1211 fn restore_terminals_from_workspace(
1213 &mut self,
1214 terminals: &[SerializedTerminalWorkspace],
1215 ) -> HashMap<usize, BufferId> {
1216 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
1217 if terminals.is_empty() {
1218 return terminal_buffer_map;
1219 }
1220 let __window_bridge = self.active_window().bridge.clone();
1221 self.active_window_mut()
1222 .terminal_manager
1223 .set_async_bridge(__window_bridge);
1224 for terminal in terminals {
1225 if let Some(buffer_id) = self.restore_terminal_from_workspace(terminal) {
1226 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
1227 }
1228 }
1229 terminal_buffer_map
1230 }
1231
1232 fn restore_bookmarks_from_workspace(
1234 &mut self,
1235 bookmarks: &HashMap<char, SerializedBookmark>,
1236 path_to_buffer: &HashMap<PathBuf, BufferId>,
1237 ) {
1238 for (key, bookmark) in bookmarks {
1239 let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) else {
1240 continue;
1241 };
1242 if let Some(buffer) = self
1243 .windows
1244 .get(&self.active_window)
1245 .map(|w| &w.buffers)
1246 .expect("active window present")
1247 .get(&buffer_id)
1248 {
1249 let pos = bookmark.position.min(buffer.buffer.len());
1250 self.active_window_mut().bookmarks.set(
1251 *key,
1252 Bookmark {
1253 buffer_id,
1254 position: pos,
1255 },
1256 );
1257 }
1258 }
1259 }
1260
1261 fn clean_orphaned_buffers(&mut self) {
1264 let referenced: HashSet<BufferId> = self
1265 .windows
1266 .get(&self.active_window)
1267 .and_then(|w| w.buffers.splits())
1268 .map(|(_, vs)| vs)
1269 .expect("active window must have a populated split layout")
1270 .values()
1271 .flat_map(|vs| vs.buffer_tab_ids())
1272 .collect();
1273 let orphans: Vec<BufferId> = self
1274 .windows
1275 .get(&self.active_window)
1276 .map(|w| &w.buffers)
1277 .expect("active window present")
1278 .iter()
1279 .filter(|(id, state)| {
1280 !referenced.contains(id)
1281 && state.buffer.file_path().is_none()
1282 && !state.buffer.is_modified()
1283 })
1284 .map(|(id, _)| *id)
1285 .collect();
1286 for id in orphans {
1287 tracing::debug!("Removing orphaned empty unnamed buffer {:?}", id);
1288 self.windows
1289 .get_mut(&self.active_window)
1290 .map(|w| &mut w.buffers)
1291 .expect("active window present")
1292 .remove(&id);
1293 self.detach_buffer_from_all_windows(id);
1294 self.active_window_mut().event_logs.remove(&id);
1295 self.active_window_mut().buffer_metadata.remove(&id);
1296 }
1297 }
1298
1299 fn log_restore_summary(&mut self) {
1302 tracing::debug!(
1303 "Workspace restore complete: {} splits, {} buffers",
1304 self.windows
1305 .get(&self.active_window)
1306 .and_then(|w| w.buffers.splits())
1307 .map(|(_, vs)| vs)
1308 .expect("active window must have a populated split layout")
1309 .len(),
1310 self.windows
1311 .get(&self.active_window)
1312 .map(|w| &w.buffers)
1313 .expect("active window present")
1314 .len()
1315 );
1316 let restored_count = self.buffers().count_where(|id, _| {
1317 self.active_window()
1318 .buffer_metadata
1319 .get(&id)
1320 .is_some_and(|m| !m.hidden_from_tabs && !m.is_virtual())
1321 });
1322 if restored_count == 0 {
1323 return;
1324 }
1325 let msg = match self
1326 .session_name
1327 .as_ref()
1328 .map(|n| format!("session '{}'", n))
1329 {
1330 Some(label) => format!("Restored {} ({} buffer(s))", label, restored_count),
1331 None => format!(
1332 "Restored {} buffer(s) from previous session",
1333 restored_count
1334 ),
1335 };
1336 self.set_status_message(msg);
1337 }
1338
1339 fn restore_terminal_from_workspace(
1348 &mut self,
1349 terminal: &SerializedTerminalWorkspace,
1350 ) -> Option<BufferId> {
1351 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
1353 let log_path = if terminal.log_path.is_absolute() {
1354 terminal.log_path.clone()
1355 } else {
1356 terminals_root.join(&terminal.log_path)
1357 };
1358 let backing_path = if terminal.backing_path.is_absolute() {
1359 terminal.backing_path.clone()
1360 } else {
1361 terminals_root.join(&terminal.backing_path)
1362 };
1363
1364 #[allow(clippy::let_underscore_must_use)]
1366 let _ = self.authority.filesystem.create_dir_all(
1367 log_path
1368 .parent()
1369 .or_else(|| backing_path.parent())
1370 .unwrap_or(&terminals_root),
1371 );
1372
1373 let predicted_id = self.active_window().terminal_manager.next_terminal_id();
1375 self.active_window_mut()
1376 .terminal_log_files
1377 .insert(predicted_id, log_path.clone());
1378 self.active_window_mut()
1379 .terminal_backing_files
1380 .insert(predicted_id, backing_path.clone());
1381
1382 let wrapper_for_spawn = self.resolved_terminal_wrapper();
1384 let terminal_id = match self
1385 .windows
1386 .get_mut(&self.active_window)
1387 .map(|w| &mut w.terminal_manager)
1388 .expect("active window present")
1389 .spawn(
1390 terminal.cols,
1391 terminal.rows,
1392 terminal.cwd.clone(),
1393 Some(log_path.clone()),
1394 Some(backing_path.clone()),
1395 wrapper_for_spawn,
1396 ) {
1397 Ok(id) => id,
1398 Err(e) => {
1399 tracing::warn!(
1400 "Failed to restore terminal {}: {}",
1401 terminal.terminal_index,
1402 e
1403 );
1404 return None;
1405 }
1406 };
1407
1408 if terminal_id != predicted_id {
1410 self.active_window_mut()
1411 .terminal_log_files
1412 .insert(terminal_id, log_path.clone());
1413 self.active_window_mut()
1414 .terminal_backing_files
1415 .insert(terminal_id, backing_path.clone());
1416 self.active_window_mut()
1417 .terminal_log_files
1418 .remove(&predicted_id);
1419 self.active_window_mut()
1420 .terminal_backing_files
1421 .remove(&predicted_id);
1422 }
1423
1424 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
1426
1427 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
1430
1431 Some(buffer_id)
1432 }
1433
1434 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
1439 if !backing_path.exists() {
1441 return;
1442 }
1443
1444 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
1445 if let Ok(new_state) = EditorState::from_file_with_languages(
1446 backing_path,
1447 self.terminal_width,
1448 self.terminal_height,
1449 large_file_threshold,
1450 &self.grammar_registry,
1451 &self.config.languages,
1452 std::sync::Arc::clone(&self.authority.filesystem),
1453 ) {
1454 self.active_window_mut()
1455 .install_terminal_buffer_state(buffer_id, new_state);
1456 }
1457 }
1458
1459 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, WorkspaceError> {
1461 for (buffer_id, metadata) in &self.active_window().buffer_metadata {
1463 if let Some(file_path) = metadata.file_path() {
1464 if file_path == path {
1465 return Ok(*buffer_id);
1466 }
1467 }
1468 }
1469
1470 self.open_file(path).map_err(WorkspaceError::Io)
1472 }
1473
1474 #[allow(clippy::too_many_arguments)]
1476 fn restore_split_node(
1477 &mut self,
1478 node: &SerializedSplitNode,
1479 path_to_buffer: &HashMap<PathBuf, BufferId>,
1480 terminal_buffers: &HashMap<usize, BufferId>,
1481 unnamed_buffers: &HashMap<String, BufferId>,
1482 split_states: &HashMap<usize, SerializedSplitViewState>,
1483 split_id_map: &mut HashMap<usize, SplitId>,
1484 is_first_leaf: bool,
1485 ) {
1486 match node {
1487 SerializedSplitNode::Leaf {
1488 file_path,
1489 split_id,
1490 label,
1491 unnamed_recovery_id,
1492 role,
1493 } => {
1494 let buffer_id = file_path
1496 .as_ref()
1497 .and_then(|p| path_to_buffer.get(p).copied())
1498 .or_else(|| {
1499 unnamed_recovery_id
1500 .as_ref()
1501 .and_then(|id| unnamed_buffers.get(id).copied())
1502 })
1503 .unwrap_or(self.active_buffer());
1504
1505 let current_leaf_id = if is_first_leaf {
1506 let leaf_id = self
1508 .windows
1509 .get(&self.active_window)
1510 .and_then(|w| w.buffers.splits())
1511 .map(|(mgr, _)| mgr)
1512 .expect("active window must have a populated split layout")
1513 .active_split();
1514 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
1515 leaf_id
1516 } else {
1517 self.windows
1519 .get(&self.active_window)
1520 .and_then(|w| w.buffers.splits())
1521 .map(|(mgr, _)| mgr)
1522 .expect("active window must have a populated split layout")
1523 .active_split()
1524 };
1525
1526 split_id_map.insert(*split_id, current_leaf_id.into());
1528
1529 if let Some(label) = label {
1531 self.windows
1532 .get_mut(&self.active_window)
1533 .and_then(|w| w.split_manager_mut())
1534 .expect("active window must have a populated split layout")
1535 .set_label(current_leaf_id, label.clone());
1536 }
1537
1538 if let Some(role) = role {
1541 self.windows
1542 .get_mut(&self.active_window)
1543 .and_then(|w| w.split_manager_mut())
1544 .expect("active window must have a populated split layout")
1545 .clear_role(*role);
1546 self.windows
1547 .get_mut(&self.active_window)
1548 .and_then(|w| w.split_manager_mut())
1549 .expect("active window must have a populated split layout")
1550 .set_leaf_role(current_leaf_id, Some(*role));
1551 }
1552
1553 self.restore_split_view_state(
1555 current_leaf_id,
1556 *split_id,
1557 split_states,
1558 path_to_buffer,
1559 terminal_buffers,
1560 unnamed_buffers,
1561 );
1562 }
1563 SerializedSplitNode::Terminal {
1564 terminal_index,
1565 split_id,
1566 label,
1567 role,
1568 } => {
1569 let buffer_id = terminal_buffers
1570 .get(terminal_index)
1571 .copied()
1572 .unwrap_or(self.active_buffer());
1573
1574 let current_leaf_id = if is_first_leaf {
1575 let leaf_id = self
1576 .windows
1577 .get(&self.active_window)
1578 .and_then(|w| w.buffers.splits())
1579 .map(|(mgr, _)| mgr)
1580 .expect("active window must have a populated split layout")
1581 .active_split();
1582 self.active_window_mut().set_pane_buffer(leaf_id, buffer_id);
1583 leaf_id
1584 } else {
1585 self.windows
1586 .get(&self.active_window)
1587 .and_then(|w| w.buffers.splits())
1588 .map(|(mgr, _)| mgr)
1589 .expect("active window must have a populated split layout")
1590 .active_split()
1591 };
1592
1593 split_id_map.insert(*split_id, current_leaf_id.into());
1594
1595 if let Some(label) = label {
1597 self.windows
1598 .get_mut(&self.active_window)
1599 .and_then(|w| w.split_manager_mut())
1600 .expect("active window must have a populated split layout")
1601 .set_label(current_leaf_id, label.clone());
1602 }
1603
1604 if let Some(role) = role {
1607 self.windows
1608 .get_mut(&self.active_window)
1609 .and_then(|w| w.split_manager_mut())
1610 .expect("active window must have a populated split layout")
1611 .clear_role(*role);
1612 self.windows
1613 .get_mut(&self.active_window)
1614 .and_then(|w| w.split_manager_mut())
1615 .expect("active window must have a populated split layout")
1616 .set_leaf_role(current_leaf_id, Some(*role));
1617 }
1618
1619 self.windows
1620 .get_mut(&self.active_window)
1621 .and_then(|w| w.split_manager_mut())
1622 .expect("active window must have a populated split layout")
1623 .set_split_buffer(current_leaf_id, buffer_id);
1624
1625 self.restore_split_view_state(
1626 current_leaf_id,
1627 *split_id,
1628 split_states,
1629 path_to_buffer,
1630 terminal_buffers,
1631 unnamed_buffers,
1632 );
1633 }
1634 SerializedSplitNode::Split {
1635 direction,
1636 first,
1637 second,
1638 ratio,
1639 split_id,
1640 } => {
1641 self.restore_split_node(
1643 first,
1644 path_to_buffer,
1645 terminal_buffers,
1646 unnamed_buffers,
1647 split_states,
1648 split_id_map,
1649 is_first_leaf,
1650 );
1651
1652 let second_buffer_id = get_first_leaf_buffer(
1654 second,
1655 path_to_buffer,
1656 terminal_buffers,
1657 unnamed_buffers,
1658 )
1659 .unwrap_or(self.active_buffer());
1660
1661 let split_direction = match direction {
1663 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
1664 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
1665 };
1666
1667 match self.split_manager_mut().split_active(
1669 split_direction,
1670 second_buffer_id,
1671 *ratio,
1672 ) {
1673 Ok(new_leaf_id) => {
1674 let mut view_state = SplitViewState::with_buffer(
1676 self.terminal_width,
1677 self.terminal_height,
1678 second_buffer_id,
1679 );
1680 view_state.apply_config_defaults(
1681 self.config.editor.line_numbers,
1682 self.config.editor.highlight_current_line,
1683 self.active_window()
1684 .resolve_line_wrap_for_buffer(second_buffer_id),
1685 self.config.editor.wrap_indent,
1686 self.active_window()
1687 .resolve_wrap_column_for_buffer(second_buffer_id),
1688 self.config.editor.rulers.clone(),
1689 );
1690 self.windows
1691 .get_mut(&self.active_window)
1692 .and_then(|w| w.split_view_states_mut())
1693 .expect("active window must have a populated split layout")
1694 .insert(new_leaf_id, view_state);
1695
1696 split_id_map.insert(*split_id, new_leaf_id.into());
1698
1699 self.restore_split_node(
1701 second,
1702 path_to_buffer,
1703 terminal_buffers,
1704 unnamed_buffers,
1705 split_states,
1706 split_id_map,
1707 false,
1708 );
1709 }
1710 Err(e) => {
1711 tracing::error!("Failed to create split during workspace restore: {}", e);
1712 }
1713 }
1714 }
1715 }
1716 }
1717
1718 fn restore_split_view_state(
1720 &mut self,
1721 current_split_id: LeafId,
1722 saved_split_id: usize,
1723 split_states: &HashMap<usize, SerializedSplitViewState>,
1724 path_to_buffer: &HashMap<PathBuf, BufferId>,
1725 terminal_buffers: &HashMap<usize, BufferId>,
1726 unnamed_buffers: &HashMap<String, BufferId>,
1727 ) {
1728 let Some(split_state) = split_states.get(&saved_split_id) else {
1730 return;
1731 };
1732
1733 let split_buf_for_current = self.split_manager().buffer_for_split(current_split_id);
1737 let active_id = self.active_window;
1738 let active_buffer_id = self
1739 .windows
1740 .get_mut(&active_id)
1741 .expect("active window must exist")
1742 .buffers
1743 .with_all_mut(|__buffers_mut, _mgr, vs_map| {
1744 let Some(view_state) = vs_map.get_mut(¤t_split_id) else {
1745 return None;
1746 };
1747 let mut active_buffer_id: Option<BufferId> = None;
1748 if !split_state.open_tabs.is_empty() {
1749 view_state.open_buffers.clear();
1752
1753 for tab in &split_state.open_tabs {
1754 match tab {
1755 SerializedTabRef::File(rel_path) => {
1756 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1757 if !view_state.has_buffer(buffer_id) {
1758 view_state.add_buffer(buffer_id);
1759 }
1760 view_state.ensure_buffer_state(buffer_id);
1762 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
1763 view_state
1764 .buffer_state_mut(buffer_id)
1765 .unwrap()
1766 .viewport
1767 .line_wrap_enabled = false;
1768 }
1769 }
1770 }
1771 SerializedTabRef::Terminal(index) => {
1772 if let Some(&buffer_id) = terminal_buffers.get(index) {
1773 if !view_state.has_buffer(buffer_id) {
1774 view_state.add_buffer(buffer_id);
1775 }
1776 view_state
1777 .ensure_buffer_state(buffer_id)
1778 .viewport
1779 .line_wrap_enabled = false;
1780 }
1781 }
1782 SerializedTabRef::Unnamed(recovery_id) => {
1783 if let Some(&buffer_id) = unnamed_buffers.get(recovery_id) {
1784 if !view_state.has_buffer(buffer_id) {
1785 view_state.add_buffer(buffer_id);
1786 }
1787 view_state.ensure_buffer_state(buffer_id);
1788 }
1789 }
1790 }
1791 }
1792
1793 if view_state.open_buffers.is_empty() {
1798 if let Some(buf) = split_buf_for_current {
1799 view_state.add_buffer(buf);
1800 view_state.ensure_buffer_state(buf);
1801 }
1802 }
1803
1804 if let Some(active_idx) = split_state.active_tab_index {
1805 if let Some(tab) = split_state.open_tabs.get(active_idx) {
1806 active_buffer_id = match tab {
1807 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
1808 SerializedTabRef::Terminal(index) => {
1809 terminal_buffers.get(index).copied()
1810 }
1811 SerializedTabRef::Unnamed(id) => unnamed_buffers.get(id).copied(),
1812 };
1813 }
1814 }
1815 } else {
1816 for rel_path in &split_state.open_files {
1818 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
1819 if !view_state.has_buffer(buffer_id) {
1820 view_state.add_buffer(buffer_id);
1821 }
1822 view_state.ensure_buffer_state(buffer_id);
1823 }
1824 }
1825
1826 let active_file_path =
1827 split_state.open_files.get(split_state.active_file_index);
1828 active_buffer_id =
1829 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
1830 }
1831
1832 for (rel_path, file_state) in &split_state.file_states {
1834 let rel_str = rel_path.to_string_lossy();
1836 let buffer_id = if let Some(recovery_id) = rel_str.strip_prefix("__unnamed__") {
1837 match unnamed_buffers.get(recovery_id).copied() {
1838 Some(id) => id,
1839 None => continue,
1840 }
1841 } else {
1842 match path_to_buffer.get(rel_path).copied() {
1843 Some(id) => id,
1844 None => continue,
1845 }
1846 };
1847 let max_pos = __buffers_mut
1848 .get(&buffer_id)
1849 .map(|b| b.buffer.len())
1850 .unwrap_or(0);
1851
1852 let buf_state = view_state.ensure_buffer_state(buffer_id);
1854
1855 let cursor_pos = file_state.cursor.position.min(max_pos);
1856 buf_state.cursors.primary_mut().position = cursor_pos;
1857 buf_state.cursors.primary_mut().anchor =
1858 file_state.cursor.anchor.map(|a| a.min(max_pos));
1859 buf_state.cursors.primary_mut().sticky_column = file_state.cursor.sticky_column;
1860
1861 buf_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
1862 buf_state.viewport.top_view_line_offset =
1863 file_state.scroll.top_view_line_offset;
1864 buf_state.viewport.left_column = file_state.scroll.left_column;
1865 buf_state.viewport.set_skip_resize_sync();
1866
1867 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1875 super::navigation::reconcile_restored_buffer_view(
1876 buf_state,
1877 &mut state.buffer,
1878 );
1879 }
1880
1881 buf_state.view_mode = match file_state.view_mode {
1883 SerializedViewMode::Source => ViewMode::Source,
1884 SerializedViewMode::PageView => ViewMode::PageView,
1885 };
1886 buf_state.compose_width = file_state.compose_width;
1887 buf_state.plugin_state = file_state.plugin_state.clone();
1888 if let Some(state) = __buffers_mut.get_mut(&buffer_id) {
1889 buf_state.folds.clear(&mut state.marker_list);
1890 for fold in &file_state.folds {
1891 let Some(resolved_header) = resolve_fold_header_line(
1898 &state.buffer,
1899 fold.header_line,
1900 fold.header_text.as_deref(),
1901 ) else {
1902 tracing::debug!(
1903 "Dropping stale fold: header_line={} no longer matches stored \
1904 header_text after external edit",
1905 fold.header_line,
1906 );
1907 continue;
1908 };
1909
1910 let shift = resolved_header as i64 - fold.header_line as i64;
1912 let adjusted_end = (fold.end_line as i64 + shift).max(0) as usize;
1913 let start_line = resolved_header.saturating_add(1);
1914 let end_line = adjusted_end;
1915 if start_line > end_line {
1916 continue;
1917 }
1918 let Some(start_byte) = state.buffer.line_start_offset(start_line)
1919 else {
1920 continue;
1921 };
1922 let end_byte = state
1923 .buffer
1924 .line_start_offset(end_line.saturating_add(1))
1925 .unwrap_or_else(|| state.buffer.len());
1926 buf_state.folds.add(
1927 &mut state.marker_list,
1928 start_byte,
1929 end_byte,
1930 fold.placeholder.clone(),
1931 );
1932 }
1933 }
1934
1935 tracing::trace!(
1936 "Restored keyed state for {:?}: cursor={}, top_byte={}, view_mode={:?}",
1937 rel_path,
1938 cursor_pos,
1939 buf_state.viewport.top_byte,
1940 buf_state.view_mode,
1941 );
1942 }
1943
1944 let restored_view_mode = match split_state.view_mode {
1947 SerializedViewMode::Source => ViewMode::Source,
1948 SerializedViewMode::PageView => ViewMode::PageView,
1949 };
1950
1951 if let Some(active_buf_id) = active_buffer_id {
1952 view_state.switch_buffer(active_buf_id);
1954
1955 let active_has_file_state = split_state.file_states.keys().any(|rel_path| {
1957 path_to_buffer.get(rel_path).copied() == Some(active_buf_id)
1958 });
1959 if !active_has_file_state {
1960 view_state.active_state_mut().view_mode = restored_view_mode.clone();
1961 view_state.active_state_mut().compose_width = split_state.compose_width;
1962 }
1963
1964 }
1966 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1967 active_buffer_id
1968 })
1969 .flatten();
1970
1971 if let Some(active_buf_id) = active_buffer_id {
1975 self.windows
1976 .get_mut(&active_id)
1977 .and_then(|w| w.split_manager_mut())
1978 .expect("active window must have a populated split layout")
1979 .set_split_buffer(current_split_id, active_buf_id);
1980 }
1981 }
1982
1983 pub fn save_all_windows_workspaces(&mut self) -> Result<(), WorkspaceError> {
1996 let originally_active = self.active_window;
1997 let originally_wd = self.working_dir.clone();
1998
1999 let targets: Vec<(fresh_core::WindowId, PathBuf)> = self
2000 .windows
2001 .iter()
2002 .filter(|(_, w)| w.buffers.splits().is_some())
2003 .map(|(id, w)| (*id, w.root.clone()))
2004 .collect();
2005
2006 let mut first_err = None;
2007 for (id, root) in targets {
2008 self.active_window = id;
2009 self.working_dir = root;
2010 if let Err(e) = self.save_workspace() {
2011 tracing::warn!("Failed to save workspace for window {id}: {e}");
2012 if first_err.is_none() {
2013 first_err = Some(e);
2014 }
2015 }
2016 }
2017
2018 self.active_window = originally_active;
2019 self.working_dir = originally_wd;
2020
2021 match first_err {
2022 Some(e) => Err(e),
2023 None => Ok(()),
2024 }
2025 }
2026
2027 pub fn restore_inactive_window_workspaces(&mut self) {
2043 let originally_active = self.active_window;
2044 let originally_wd = self.working_dir.clone();
2045 let saved_plugin_state = self.plugin_global_state.clone();
2046
2047 let targets: Vec<(fresh_core::WindowId, PathBuf)> = self
2048 .windows
2049 .iter()
2050 .filter(|(id, _)| **id != originally_active)
2051 .map(|(id, w)| (*id, w.root.clone()))
2052 .collect();
2053
2054 for (id, root) in targets {
2055 self.active_window = id;
2056 self.working_dir = root;
2057 match self.try_restore_workspace() {
2058 Ok(true) => tracing::debug!("Restored workspace for inactive window {id}"),
2059 Ok(false) => tracing::trace!(
2060 "No persisted workspace for inactive window {id}; seed layout kept"
2061 ),
2062 Err(e) => {
2063 tracing::warn!("Failed to restore workspace for inactive window {id}: {e}")
2064 }
2065 }
2066 }
2067
2068 self.active_window = originally_active;
2069 self.working_dir = originally_wd;
2070 self.plugin_global_state = saved_plugin_state;
2071 }
2072}
2073
2074fn get_first_leaf_buffer(
2076 node: &SerializedSplitNode,
2077 path_to_buffer: &HashMap<PathBuf, BufferId>,
2078 terminal_buffers: &HashMap<usize, BufferId>,
2079 unnamed_buffers: &HashMap<String, BufferId>,
2080) -> Option<BufferId> {
2081 match node {
2082 SerializedSplitNode::Leaf {
2083 file_path,
2084 unnamed_recovery_id,
2085 ..
2086 } => file_path
2087 .as_ref()
2088 .and_then(|p| path_to_buffer.get(p).copied())
2089 .or_else(|| {
2090 unnamed_recovery_id
2091 .as_ref()
2092 .and_then(|id| unnamed_buffers.get(id).copied())
2093 }),
2094 SerializedSplitNode::Terminal { terminal_index, .. } => {
2095 terminal_buffers.get(terminal_index).copied()
2096 }
2097 SerializedSplitNode::Split { first, .. } => {
2098 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers, unnamed_buffers)
2099 }
2100 }
2101}
2102
2103fn serialize_split_node(
2108 node: &SplitNode,
2109 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2110 working_dir: &Path,
2111 terminal_buffers: &HashMap<BufferId, TerminalId>,
2112 terminal_indices: &HashMap<TerminalId, usize>,
2113 split_labels: &HashMap<SplitId, String>,
2114) -> SerializedSplitNode {
2115 serialize_split_node_pruned(
2116 node,
2117 buffer_metadata,
2118 working_dir,
2119 terminal_buffers,
2120 terminal_indices,
2121 split_labels,
2122 )
2123 .unwrap_or({
2124 SerializedSplitNode::Leaf {
2127 file_path: None,
2128 split_id: 0,
2129 label: None,
2130 unnamed_recovery_id: None,
2131 role: None,
2132 }
2133 })
2134}
2135
2136fn serialize_split_node_pruned(
2143 node: &SplitNode,
2144 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2145 working_dir: &Path,
2146 terminal_buffers: &HashMap<BufferId, TerminalId>,
2147 terminal_indices: &HashMap<TerminalId, usize>,
2148 split_labels: &HashMap<SplitId, String>,
2149) -> Option<SerializedSplitNode> {
2150 match node {
2151 SplitNode::Grouped { layout, .. } => {
2152 serialize_split_node_pruned(
2156 layout,
2157 buffer_metadata,
2158 working_dir,
2159 terminal_buffers,
2160 terminal_indices,
2161 split_labels,
2162 )
2163 }
2164 SplitNode::Leaf {
2165 buffer_id,
2166 split_id,
2167 role,
2168 } => {
2169 let raw_split_id: SplitId = (*split_id).into();
2170 let label = split_labels.get(&raw_split_id).cloned();
2171 let role = *role;
2172
2173 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2174 if let Some(index) = terminal_indices.get(terminal_id) {
2175 return Some(SerializedSplitNode::Terminal {
2176 terminal_index: *index,
2177 split_id: raw_split_id.0,
2178 label,
2179 role,
2180 });
2181 }
2182 }
2183
2184 let meta = buffer_metadata.get(buffer_id);
2185
2186 if meta.map(|m| m.is_virtual()).unwrap_or(false) {
2190 return None;
2191 }
2192
2193 let file_path = meta.and_then(|m| m.file_path()).and_then(|abs_path| {
2194 if abs_path.as_os_str().is_empty() {
2195 None } else {
2197 abs_path
2198 .strip_prefix(working_dir)
2199 .ok()
2200 .map(|p| p.to_path_buf())
2201 }
2202 });
2203
2204 let unnamed_recovery_id = if file_path.is_none() {
2207 meta.and_then(|m| m.recovery_id.clone())
2208 } else {
2209 None
2210 };
2211
2212 Some(SerializedSplitNode::Leaf {
2213 file_path,
2214 split_id: raw_split_id.0,
2215 label,
2216 unnamed_recovery_id,
2217 role,
2218 })
2219 }
2220 SplitNode::Split {
2221 direction,
2222 first,
2223 second,
2224 ratio,
2225 split_id,
2226 ..
2227 } => {
2228 let raw_split_id: SplitId = (*split_id).into();
2229 let first = serialize_split_node_pruned(
2230 first,
2231 buffer_metadata,
2232 working_dir,
2233 terminal_buffers,
2234 terminal_indices,
2235 split_labels,
2236 );
2237 let second = serialize_split_node_pruned(
2238 second,
2239 buffer_metadata,
2240 working_dir,
2241 terminal_buffers,
2242 terminal_indices,
2243 split_labels,
2244 );
2245 match (first, second) {
2246 (Some(f), Some(s)) => Some(SerializedSplitNode::Split {
2247 direction: match direction {
2248 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
2249 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
2250 },
2251 first: Box::new(f),
2252 second: Box::new(s),
2253 ratio: *ratio,
2254 split_id: raw_split_id.0,
2255 }),
2256 (Some(only), None) | (None, Some(only)) => Some(only),
2259 (None, None) => None,
2260 }
2261 }
2262 }
2263}
2264
2265fn serialize_split_view_state(
2266 view_state: &crate::view::split::SplitViewState,
2267 buffers: &HashMap<BufferId, EditorState>,
2268 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2269 working_dir: &Path,
2270 active_buffer: Option<BufferId>,
2271 terminal_buffers: &HashMap<BufferId, TerminalId>,
2272 terminal_indices: &HashMap<TerminalId, usize>,
2273) -> SerializedSplitViewState {
2274 let mut open_tabs = Vec::new();
2275 let mut open_files = Vec::new();
2276 let mut active_tab_index = None;
2277
2278 for buffer_id in view_state.buffer_tab_ids() {
2280 let buffer_id = &buffer_id;
2281 let tab_index = open_tabs.len();
2282 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
2283 if let Some(idx) = terminal_indices.get(terminal_id) {
2284 open_tabs.push(SerializedTabRef::Terminal(*idx));
2285 if Some(*buffer_id) == active_buffer {
2286 active_tab_index = Some(tab_index);
2287 }
2288 continue;
2289 }
2290 }
2291
2292 if let Some(meta) = buffer_metadata.get(buffer_id) {
2293 if let Some(abs_path) = meta.file_path() {
2294 if abs_path.as_os_str().is_empty() {
2295 if let Some(ref recovery_id) = meta.recovery_id {
2297 open_tabs.push(SerializedTabRef::Unnamed(recovery_id.clone()));
2298 if Some(*buffer_id) == active_buffer {
2299 active_tab_index = Some(tab_index);
2300 }
2301 }
2302 } else if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
2303 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
2304 open_files.push(rel_path.to_path_buf());
2305 if Some(*buffer_id) == active_buffer {
2306 active_tab_index = Some(tab_index);
2307 }
2308 } else {
2309 open_tabs.push(SerializedTabRef::File(abs_path.to_path_buf()));
2311 if Some(*buffer_id) == active_buffer {
2312 active_tab_index = Some(tab_index);
2313 }
2314 }
2315 }
2316 }
2317 }
2318
2319 let active_file_index = active_tab_index
2321 .and_then(|idx| open_tabs.get(idx))
2322 .and_then(|tab| match tab {
2323 SerializedTabRef::File(path) => {
2324 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
2325 }
2326 _ => None,
2327 })
2328 .unwrap_or(0);
2329
2330 let mut file_states = HashMap::new();
2332 for (buffer_id, buf_state) in &view_state.keyed_states {
2333 let Some(meta) = buffer_metadata.get(buffer_id) else {
2334 continue;
2335 };
2336 let Some(abs_path) = meta.file_path() else {
2337 continue;
2338 };
2339
2340 let state_key = if abs_path.as_os_str().is_empty() {
2342 if let Some(ref recovery_id) = meta.recovery_id {
2344 PathBuf::from(format!("__unnamed__{}", recovery_id))
2345 } else {
2346 continue;
2347 }
2348 } else if let Ok(rp) = abs_path.strip_prefix(working_dir) {
2349 rp.to_path_buf()
2350 } else {
2351 abs_path.to_path_buf()
2353 };
2354
2355 let primary_cursor = buf_state.cursors.primary();
2356 let folds = buffers
2357 .get(buffer_id)
2358 .map(|state| {
2359 buf_state
2360 .folds
2361 .collapsed_line_ranges(&state.buffer, &state.marker_list)
2362 .into_iter()
2363 .map(|range| SerializedFoldRange {
2364 header_line: range.header_line,
2365 end_line: range.end_line,
2366 placeholder: range.placeholder,
2367 header_text: range.header_text,
2368 })
2369 .collect::<Vec<_>>()
2370 })
2371 .unwrap_or_default();
2372
2373 file_states.insert(
2374 state_key,
2375 SerializedFileState {
2376 cursor: SerializedCursor {
2377 position: primary_cursor.position,
2378 anchor: primary_cursor.anchor,
2379 sticky_column: primary_cursor.sticky_column,
2380 },
2381 additional_cursors: buf_state
2382 .cursors
2383 .iter()
2384 .skip(1) .map(|(_, cursor)| SerializedCursor {
2386 position: cursor.position,
2387 anchor: cursor.anchor,
2388 sticky_column: cursor.sticky_column,
2389 })
2390 .collect(),
2391 scroll: SerializedScroll {
2392 top_byte: buf_state.viewport.top_byte,
2393 top_view_line_offset: buf_state.viewport.top_view_line_offset,
2394 left_column: buf_state.viewport.left_column,
2395 },
2396 view_mode: match buf_state.view_mode {
2397 ViewMode::Source => SerializedViewMode::Source,
2398 ViewMode::PageView => SerializedViewMode::PageView,
2399 },
2400 compose_width: buf_state.compose_width,
2401 plugin_state: buf_state.plugin_state.clone(),
2402 folds,
2403 },
2404 );
2405 }
2406
2407 let active_view_mode = active_buffer
2409 .and_then(|id| view_state.keyed_states.get(&id))
2410 .map(|bs| match bs.view_mode {
2411 ViewMode::Source => SerializedViewMode::Source,
2412 ViewMode::PageView => SerializedViewMode::PageView,
2413 })
2414 .unwrap_or(SerializedViewMode::Source);
2415 let active_compose_width = active_buffer
2416 .and_then(|id| view_state.keyed_states.get(&id))
2417 .and_then(|bs| bs.compose_width);
2418
2419 SerializedSplitViewState {
2420 open_tabs,
2421 active_tab_index,
2422 open_files,
2423 active_file_index,
2424 file_states,
2425 tab_scroll_offset: view_state.tab_scroll_offset,
2426 view_mode: active_view_mode,
2427 compose_width: active_compose_width,
2428 }
2429}
2430
2431fn serialize_bookmarks(
2432 bookmarks: &BookmarkState,
2433 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
2434 working_dir: &Path,
2435) -> HashMap<char, SerializedBookmark> {
2436 bookmarks
2437 .iter()
2438 .filter_map(|(key, bookmark)| {
2439 buffer_metadata
2440 .get(&bookmark.buffer_id)
2441 .and_then(|meta| meta.file_path())
2442 .and_then(|abs_path| {
2443 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
2444 (
2445 key,
2446 SerializedBookmark {
2447 file_path: rel_path.to_path_buf(),
2448 position: bookmark.position,
2449 },
2450 )
2451 })
2452 })
2453 })
2454 .collect()
2455}
2456
2457fn collect_file_paths_from_states(
2459 split_states: &HashMap<usize, SerializedSplitViewState>,
2460) -> Vec<PathBuf> {
2461 let mut paths = Vec::new();
2462 for state in split_states.values() {
2463 if !state.open_tabs.is_empty() {
2464 for tab in &state.open_tabs {
2465 if let SerializedTabRef::File(path) = tab {
2466 if !paths.contains(path) {
2467 paths.push(path.clone());
2468 }
2469 }
2470 }
2471 } else {
2472 for path in &state.open_files {
2473 if !paths.contains(path) {
2474 paths.push(path.clone());
2475 }
2476 }
2477 }
2478 }
2479 paths
2480}
2481
2482fn get_expanded_dirs(
2484 explorer: &crate::view::file_tree::FileTreeView,
2485 working_dir: &Path,
2486) -> Vec<PathBuf> {
2487 let mut expanded = Vec::new();
2488 let tree = explorer.tree();
2489
2490 for node in tree.all_nodes() {
2492 if node.is_expanded() && node.is_dir() {
2493 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
2495 expanded.push(rel_path.to_path_buf());
2496 }
2497 }
2498 }
2499
2500 expanded
2501}