History manager (#26369)
While working on implementing `add_recent_documents` for Windows, I found that the process is significantly more complex compared to macOS. On macOS, simply registering the `add_recent_documents` function is enough, as the system handles everything automatically. On Windows, however, there are two cases to consider: - **Files opened by the app**: These appear in the "Recent" section (as shown in the screenshot, "test.txt") and are managed automatically by Windows (by setting windows registry), similar to macOS.  - **Folders opened by the app**: This is more complicated because Windows does not handle it automatically, requiring the application to track opened folders manually. To address this, this PR introduces a `History Manager` along with `HistoryManagerEvent::Update` and `HistoryManagerEvent::Delete` events to simplify the process of managing recently opened folders. https://github.com/user-attachments/assets/a2581c15-7653-4faf-96b0-7c48ab1dcc8d Release Notes: - N/A --------- Co-authored-by: Mikayla Maki <mikayla@zed.dev>
This commit is contained in:
parent
5734ffbb18
commit
a5fe6d1e61
14 changed files with 482 additions and 66 deletions
|
@ -67,6 +67,9 @@ uuid.workspace = true
|
|||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
@ -78,5 +81,5 @@ gpui = { workspace = true, features = ["test-support"] }
|
|||
project = { workspace = true, features = ["test-support"] }
|
||||
session = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
|
|
129
crates/workspace/src/history_manager.rs
Normal file
129
crates/workspace/src/history_manager.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use gpui::{AppContext, Entity, Global, MenuItem};
|
||||
use smallvec::SmallVec;
|
||||
use ui::App;
|
||||
use util::{ResultExt, paths::PathExt};
|
||||
|
||||
use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let manager = cx.new(|_| HistoryManager::new());
|
||||
HistoryManager::set_global(manager.clone(), cx);
|
||||
HistoryManager::init(manager, cx);
|
||||
}
|
||||
|
||||
pub struct HistoryManager {
|
||||
/// The history of workspaces that have been opened in the past, in reverse order.
|
||||
/// The most recent workspace is at the end of the vector.
|
||||
history: Vec<HistoryManagerEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HistoryManagerEntry {
|
||||
pub id: WorkspaceId,
|
||||
pub path: SmallVec<[PathBuf; 2]>,
|
||||
}
|
||||
|
||||
struct GlobalHistoryManager(Entity<HistoryManager>);
|
||||
|
||||
impl Global for GlobalHistoryManager {}
|
||||
|
||||
impl HistoryManager {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
history: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn init(this: Entity<HistoryManager>, cx: &App) {
|
||||
cx.spawn(async move |cx| {
|
||||
let recent_folders = WORKSPACE_DB
|
||||
.recent_workspaces_on_disk()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|(id, location)| HistoryManagerEntry::new(id, &location))
|
||||
.collect::<Vec<_>>();
|
||||
this.update(cx, |this, cx| {
|
||||
this.history = recent_folders;
|
||||
this.update_jump_list(cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalHistoryManager>()
|
||||
.map(|model| model.0.clone())
|
||||
}
|
||||
|
||||
fn set_global(history_manager: Entity<Self>, cx: &mut App) {
|
||||
cx.set_global(GlobalHistoryManager(history_manager));
|
||||
}
|
||||
|
||||
pub fn update_history(&mut self, id: WorkspaceId, entry: HistoryManagerEntry, cx: &App) {
|
||||
if let Some(pos) = self.history.iter().position(|e| e.id == id) {
|
||||
self.history.remove(pos);
|
||||
}
|
||||
self.history.push(entry);
|
||||
self.update_jump_list(cx);
|
||||
}
|
||||
|
||||
pub fn delete_history(&mut self, id: WorkspaceId, cx: &App) {
|
||||
let Some(pos) = self.history.iter().position(|e| e.id == id) else {
|
||||
return;
|
||||
};
|
||||
self.history.remove(pos);
|
||||
self.update_jump_list(cx);
|
||||
}
|
||||
|
||||
fn update_jump_list(&mut self, cx: &App) {
|
||||
let menus = vec![MenuItem::action("New Window", NewWindow)];
|
||||
let entries = self
|
||||
.history
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|entry| entry.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let user_removed = cx.update_jump_list(menus, entries);
|
||||
self.remove_user_removed_workspaces(user_removed, cx);
|
||||
}
|
||||
|
||||
pub fn remove_user_removed_workspaces(
|
||||
&mut self,
|
||||
user_removed: Vec<SmallVec<[PathBuf; 2]>>,
|
||||
cx: &App,
|
||||
) {
|
||||
if user_removed.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut deleted_ids = Vec::new();
|
||||
for idx in (0..self.history.len()).rev() {
|
||||
if let Some(entry) = self.history.get(idx) {
|
||||
if user_removed.contains(&entry.path) {
|
||||
deleted_ids.push(entry.id);
|
||||
self.history.remove(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.spawn(async move |_| {
|
||||
for id in deleted_ids.iter() {
|
||||
WORKSPACE_DB.delete_workspace_by_id(*id).await.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryManagerEntry {
|
||||
pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self {
|
||||
let path = location
|
||||
.sorted_paths()
|
||||
.iter()
|
||||
.map(|path| path.compact())
|
||||
.collect::<SmallVec<[PathBuf; 2]>>();
|
||||
Self { id, path }
|
||||
}
|
||||
}
|
|
@ -745,7 +745,7 @@ impl WorkspaceDb {
|
|||
conn.exec_bound(sql!(
|
||||
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||
.context("Clearing old panes")?;
|
||||
.context("Clearing old panes")?;
|
||||
|
||||
conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod dock;
|
||||
pub mod history_manager;
|
||||
pub mod item;
|
||||
mod modal_layer;
|
||||
pub mod notifications;
|
||||
|
@ -43,6 +44,7 @@ use gpui::{
|
|||
WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as,
|
||||
impl_actions, point, relative, size, transparent_black,
|
||||
};
|
||||
pub use history_manager::*;
|
||||
pub use item::{
|
||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||
ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
|
||||
|
@ -387,6 +389,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
component::init();
|
||||
theme_preview::init(cx);
|
||||
toast_layer::init(cx);
|
||||
history_manager::init(cx);
|
||||
|
||||
cx.on_action(Workspace::close_global);
|
||||
cx.on_action(reload);
|
||||
|
@ -902,6 +905,9 @@ impl Workspace {
|
|||
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
|
||||
this.update_window_title(window, cx);
|
||||
this.serialize_workspace(window, cx);
|
||||
// This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
|
||||
// So we need to update the history.
|
||||
this.update_history(cx);
|
||||
}
|
||||
|
||||
project::Event::DisconnectedFromHost => {
|
||||
|
@ -1334,7 +1340,10 @@ impl Workspace {
|
|||
.unwrap_or_default();
|
||||
|
||||
window
|
||||
.update(cx, |_, window, _| window.activate_window())
|
||||
.update(cx, |workspace, window, cx| {
|
||||
window.activate_window();
|
||||
workspace.update_history(cx);
|
||||
})
|
||||
.log_err();
|
||||
Ok((window, opened_items))
|
||||
})
|
||||
|
@ -4707,19 +4716,7 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
let location = if let Some(ssh_project) = &self.serialized_ssh_project {
|
||||
Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
|
||||
} else if let Some(local_paths) = self.local_paths(cx) {
|
||||
if !local_paths.is_empty() {
|
||||
Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(location) = location {
|
||||
if let Some(location) = self.serialize_workspace_location(cx) {
|
||||
let breakpoints = self.project.update(cx, |project, cx| {
|
||||
project.breakpoint_store().read(cx).all_breakpoints(cx)
|
||||
});
|
||||
|
@ -4739,13 +4736,42 @@ impl Workspace {
|
|||
breakpoints,
|
||||
window_id: Some(window.window_handle().window_id().as_u64()),
|
||||
};
|
||||
|
||||
return window.spawn(cx, async move |_| {
|
||||
persistence::DB.save_workspace(serialized_workspace).await
|
||||
persistence::DB.save_workspace(serialized_workspace).await;
|
||||
});
|
||||
}
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn serialize_workspace_location(&self, cx: &App) -> Option<SerializedWorkspaceLocation> {
|
||||
if let Some(ssh_project) = &self.serialized_ssh_project {
|
||||
Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
|
||||
} else if let Some(local_paths) = self.local_paths(cx) {
|
||||
if !local_paths.is_empty() {
|
||||
Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_history(&self, cx: &mut App) {
|
||||
let Some(id) = self.database_id() else {
|
||||
return;
|
||||
};
|
||||
let Some(location) = self.serialize_workspace_location(cx) else {
|
||||
return;
|
||||
};
|
||||
if let Some(manager) = HistoryManager::global(cx) {
|
||||
manager.update(cx, |this, cx| {
|
||||
this.update_history(id, HistoryManagerEntry::new(id, &location), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn serialize_items(
|
||||
this: &WeakEntity<Self>,
|
||||
items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
|
||||
|
@ -6614,6 +6640,7 @@ async fn open_ssh_project_inner(
|
|||
let mut workspace =
|
||||
Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
|
||||
workspace.set_serialized_ssh_project(serialized_ssh_project);
|
||||
workspace.update_history(cx);
|
||||
workspace
|
||||
});
|
||||
})?;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue