From f930969411f4d0007aefca0beb2cbf3dc74d721e Mon Sep 17 00:00:00 2001 From: Tung Hoang Date: Wed, 21 Feb 2024 15:30:02 -0800 Subject: [PATCH] Allow removing workspaces from the "recent projects" modal (#7885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screenshot 2024-02-19 at 10 59 01 AM Screenshot 2024-02-19 at 10 59 27 AM Release Notes: - Added a way to remove entries from the recent projects modal ([7426](https://github.com/zed-industries/zed/issues/7426)). --- crates/picker/src/picker.rs | 18 ++- crates/recent_projects/src/recent_projects.rs | 116 +++++++++++++----- crates/workspace/src/persistence.rs | 4 +- 3 files changed, 98 insertions(+), 40 deletions(-) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 32add0b509..1ade6eed1f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, list, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, Length, ListState, MouseButton, MouseDownEvent, Render, Task, + div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent, + EventEmitter, FocusHandle, FocusableView, Length, ListState, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{sync::Arc, time::Duration}; @@ -103,7 +103,7 @@ impl Picker { let mut this = Self { delegate, editor, - element_container: Self::crate_element_container(is_uniform, cx), + element_container: Self::create_element_container(is_uniform, cx), pending_update_matches: None, confirm_on_update: None, width: None, @@ -117,7 +117,7 @@ impl Picker { this } - fn crate_element_container(is_uniform: bool, cx: &mut ViewContext) -> ElementContainer { + fn create_element_container(is_uniform: bool, cx: &mut ViewContext) -> ElementContainer { if is_uniform { ElementContainer::UniformList(UniformListScrollHandle::new()) } else { @@ -311,12 +311,10 @@ impl Picker { fn render_element(&self, cx: &mut ViewContext, ix: usize) -> impl IntoElement { div() - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, event: &MouseDownEvent, cx| { - this.handle_click(ix, event.modifiers.command, cx) - }), - ) + .id(("item", ix)) + .on_click(cx.listener(move |this, event: &ClickEvent, cx| { + this.handle_click(ix, event.down.modifiers.command, cx) + })) .children( self.delegate .render_match(ix, ix == self.delegate.selected_index(), cx), diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 0fd2552902..ac15c08eb6 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -3,16 +3,16 @@ mod projects; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task, - View, ViewContext, WeakView, + AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, + Subscription, Task, View, ViewContext, WeakView, }; use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing}; +use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip}; use util::paths::PathExt; -use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB}; +use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB}; pub use projects::OpenRecent; @@ -45,13 +45,11 @@ impl RecentProjects { let workspaces = WORKSPACE_DB .recent_workspaces_on_disk() .await - .unwrap_or_default() - .into_iter() - .map(|(_, location)| location) - .collect(); + .unwrap_or_default(); + this.update(&mut cx, move |this, cx| { this.picker.update(cx, move |picker, cx| { - picker.delegate.workspace_locations = workspaces; + picker.delegate.workspaces = workspaces; picker.update_matches(picker.query(cx), cx) }) }) @@ -124,20 +122,23 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakView, - workspace_locations: Vec, + workspaces: Vec<(WorkspaceId, WorkspaceLocation)>, selected_match_index: usize, matches: Vec, render_paths: bool, + // Flag to reset index when there is a new query vs not reset index when user delete an item + reset_selected_match_index: bool, } impl RecentProjectsDelegate { fn new(workspace: WeakView, render_paths: bool) -> Self { Self { workspace, - workspace_locations: vec![], + workspaces: vec![], selected_match_index: 0, matches: Default::default(), render_paths, + reset_selected_match_index: true, } } } @@ -169,10 +170,10 @@ impl PickerDelegate for RecentProjectsDelegate { let query = query.trim_start(); let smart_case = query.chars().any(|c| c.is_uppercase()); let candidates = self - .workspace_locations + .workspaces .iter() .enumerate() - .map(|(id, location)| { + .map(|(id, (_, location))| { let combined_string = location .paths() .iter() @@ -192,14 +193,17 @@ impl PickerDelegate for RecentProjectsDelegate { )); self.matches.sort_unstable_by_key(|m| m.candidate_id); - self.selected_match_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); + if self.reset_selected_match_index { + self.selected_match_index = self + .matches + .iter() + .enumerate() + .rev() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(ix, _)| ix) + .unwrap_or(0); + } + self.reset_selected_match_index = true; Task::ready(()) } @@ -209,7 +213,7 @@ impl PickerDelegate for RecentProjectsDelegate { .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let workspace_location = &self.workspace_locations[selected_match.candidate_id]; + let (_, workspace_location) = &self.workspaces[selected_match.candidate_id]; workspace .update(cx, |workspace, cx| { workspace @@ -226,19 +230,18 @@ impl PickerDelegate for RecentProjectsDelegate { &self, ix: usize, selected: bool, - _cx: &mut ViewContext>, + cx: &mut ViewContext>, ) -> Option { let Some(r#match) = self.matches.get(ix) else { return None; }; - let highlighted_location = HighlightedWorkspaceLocation::new( - &r#match, - &self.workspace_locations[r#match.candidate_id], - ); - + let (workspace_id, location) = &self.workspaces[r#match.candidate_id]; + let highlighted_location: HighlightedWorkspaceLocation = + HighlightedWorkspaceLocation::new(&r#match, location); let tooltip_highlighted_location = highlighted_location.clone(); + let is_current_workspace = self.is_current_workspace(*workspace_id, cx); Some( ListItem::new(ix) .inset(true) @@ -255,6 +258,27 @@ impl PickerDelegate for RecentProjectsDelegate { })) }), ) + .when(!is_current_workspace, |el| { + let delete_button = div() + .child( + IconButton::new("delete", IconName::Close) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _event, cx| { + cx.stop_propagation(); + cx.prevent_default(); + + this.delegate.delete_recent_project(ix, cx) + })) + .tooltip(|cx| Tooltip::text("Delete From Recent Projects...", cx)), + ) + .into_any_element(); + + if self.selected_index() == ix { + el.end_slot::(delete_button) + } else { + el.end_hover_slot::(delete_button) + } + }) .tooltip(move |cx| { let tooltip_highlighted_location = tooltip_highlighted_location.clone(); cx.new_view(move |_| MatchTooltip { @@ -266,6 +290,42 @@ impl PickerDelegate for RecentProjectsDelegate { } } +impl RecentProjectsDelegate { + fn delete_recent_project(&self, ix: usize, cx: &mut ViewContext>) { + if let Some(selected_match) = self.matches.get(ix) { + let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; + cx.spawn(move |this, mut cx| async move { + let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; + let workspaces = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default(); + this.update(&mut cx, move |picker, cx| { + picker.delegate.workspaces = workspaces; + picker.delegate.set_selected_index(ix - 1, cx); + picker.delegate.reset_selected_match_index = false; + picker.update_matches(picker.query(cx), cx) + }) + }) + .detach(); + } + } + + fn is_current_workspace( + &self, + workspace_id: WorkspaceId, + cx: &mut ViewContext>, + ) -> bool { + if let Some(workspace) = self.workspace.upgrade() { + let workspace = workspace.read(cx); + if workspace_id == workspace.database_id() { + return true; + } + } + + false + } +} struct MatchTooltip { highlighted_location: HighlightedWorkspaceLocation, } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 2c6bf95c60..d02154a6aa 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -430,7 +430,7 @@ impl WorkspaceDb { } query! { - async fn delete_stale_workspace(id: WorkspaceId) -> Result<()> { + pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { DELETE FROM workspaces WHERE workspace_id IS ? } @@ -447,7 +447,7 @@ impl WorkspaceDb { { result.push((id, location)); } else { - delete_tasks.push(self.delete_stale_workspace(id)); + delete_tasks.push(self.delete_workspace_by_id(id)); } }