1use std::{path::PathBuf, sync::Arc};
2
3use fs::Fs;
4use gpui::{AppContext, Entity, Global, MenuItem};
5use smallvec::SmallVec;
6use ui::{App, Context};
7use util::{ResultExt, paths::PathExt};
8
9use crate::{
10 NewWindow, SerializedWorkspaceLocation, WorkspaceId, path_list::PathList,
11 persistence::WorkspaceDb,
12};
13
14pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
15 let manager = cx.new(|_| HistoryManager::new());
16 HistoryManager::set_global(manager.clone(), cx);
17 HistoryManager::init(manager, fs, cx);
18}
19
20pub struct HistoryManager {
21 /// The history of workspaces that have been opened in the past, in reverse order.
22 /// The most recent workspace is at the end of the vector.
23 history: Vec<HistoryManagerEntry>,
24}
25
26#[derive(Debug)]
27pub struct HistoryManagerEntry {
28 pub id: WorkspaceId,
29 pub path: SmallVec<[PathBuf; 2]>,
30}
31
32struct GlobalHistoryManager(Entity<HistoryManager>);
33
34impl Global for GlobalHistoryManager {}
35
36impl HistoryManager {
37 fn new() -> Self {
38 Self {
39 history: Vec::new(),
40 }
41 }
42
43 fn init(this: Entity<HistoryManager>, fs: Arc<dyn Fs>, cx: &App) {
44 let db = WorkspaceDb::global(cx);
45 cx.spawn(async move |cx| {
46 let recent_folders = db
47 .recent_workspaces_on_disk(fs.as_ref())
48 .await
49 .unwrap_or_default()
50 .into_iter()
51 .rev()
52 .filter_map(|(id, location, paths, _timestamp)| {
53 if matches!(location, SerializedWorkspaceLocation::Local) {
54 Some(HistoryManagerEntry::new(id, &paths))
55 } else {
56 None
57 }
58 })
59 .collect::<Vec<_>>();
60 this.update(cx, |this, cx| {
61 this.history = recent_folders;
62 this.update_jump_list(cx);
63 })
64 })
65 .detach();
66 }
67
68 pub fn global(cx: &App) -> Option<Entity<Self>> {
69 cx.try_global::<GlobalHistoryManager>()
70 .map(|model| model.0.clone())
71 }
72
73 fn set_global(history_manager: Entity<Self>, cx: &mut App) {
74 cx.set_global(GlobalHistoryManager(history_manager));
75 }
76
77 pub fn update_history(
78 &mut self,
79 id: WorkspaceId,
80 entry: HistoryManagerEntry,
81 cx: &mut Context<'_, HistoryManager>,
82 ) {
83 if let Some(pos) = self.history.iter().position(|e| e.id == id) {
84 self.history.remove(pos);
85 }
86 self.history.push(entry);
87 self.update_jump_list(cx);
88 }
89
90 pub fn delete_history(&mut self, id: WorkspaceId, cx: &mut Context<'_, HistoryManager>) {
91 let Some(pos) = self.history.iter().position(|e| e.id == id) else {
92 return;
93 };
94 self.history.remove(pos);
95 self.update_jump_list(cx);
96 }
97
98 fn update_jump_list(&mut self, cx: &mut Context<'_, HistoryManager>) {
99 let menus = vec![MenuItem::action("New Window", NewWindow)];
100 let entries = self
101 .history
102 .iter()
103 .rev()
104 .map(|entry| entry.path.clone())
105 .collect::<Vec<_>>();
106 let user_removed = cx.update_jump_list(menus, entries);
107 let db = WorkspaceDb::global(cx);
108 cx.spawn(async move |this, cx| {
109 let user_removed = user_removed.await;
110 if user_removed.is_empty() {
111 return;
112 }
113 let mut deleted_ids = Vec::new();
114 if let Ok(()) = this.update(cx, |this, _| {
115 for idx in (0..this.history.len()).rev() {
116 if let Some(entry) = this.history.get(idx)
117 && user_removed.contains(&entry.path)
118 {
119 deleted_ids.push(entry.id);
120 this.history.remove(idx);
121 }
122 }
123 }) {
124 for id in deleted_ids.iter() {
125 db.delete_workspace_by_id(*id).await.log_err();
126 }
127 }
128 })
129 .detach();
130 }
131}
132
133impl HistoryManagerEntry {
134 pub fn new(id: WorkspaceId, paths: &PathList) -> Self {
135 let path = paths
136 .ordered_paths()
137 .map(|path| path.compact())
138 .collect::<SmallVec<[PathBuf; 2]>>();
139 Self { id, path }
140 }
141}