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