From 201d513c5019bb901e580262fae9c15505b4eda6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 May 2023 17:35:06 +0300 Subject: [PATCH] Show navigation history in the file finder modal co-authored-by: Max --- Cargo.lock | 1 + crates/file_finder/src/file_finder.rs | 157 ++++++++------------------ crates/project/src/project.rs | 50 +------- crates/project/src/worktree.rs | 2 +- crates/workspace/Cargo.toml | 1 + crates/workspace/src/dock.rs | 13 ++- crates/workspace/src/pane.rs | 51 ++++++++- crates/workspace/src/workspace.rs | 52 ++++++++- 8 files changed, 162 insertions(+), 165 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce727a9c6c4af9aa1a5ecdb4c063d5144510f7aa..be8a8a74bff53786b82600812ac88bd072a19d70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8676,6 +8676,7 @@ dependencies = [ "gpui", "indoc", "install_cli", + "itertools", "language", "lazy_static", "log", diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 311882bd8e3586223781642749c13bd5882a8f1d..94b48d9e450b2629c98d677114f4344fe47a4a5d 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -25,10 +25,11 @@ pub struct FileFinderDelegate { latest_search_id: usize, latest_search_did_cancel: bool, latest_search_query: Option>, - relative_to: Option>, + currently_opened_path: Option, matches: Vec, selected: Option<(usize, Arc)>, cancel_flag: Arc, + history_items: Vec, } actions!(file_finder, [Toggle]); @@ -38,17 +39,26 @@ pub fn init(cx: &mut AppContext) { FileFinder::init(cx); } +const MAX_RECENT_SELECTIONS: usize = 20; + fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { workspace.toggle_modal(cx, |workspace, cx| { - let relative_to = workspace + let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx); + let currently_opened_path = workspace .active_item(cx) - .and_then(|item| item.project_path(cx)) - .map(|project_path| project_path.path.clone()); + .and_then(|item| item.project_path(cx)); + let project = workspace.project().clone(); let workspace = cx.handle().downgrade(); let finder = cx.add_view(|cx| { Picker::new( - FileFinderDelegate::new(workspace, project, relative_to, cx), + FileFinderDelegate::new( + workspace, + project, + currently_opened_path, + history_items, + cx, + ), cx, ) }); @@ -106,7 +116,8 @@ impl FileFinderDelegate { pub fn new( workspace: WeakViewHandle, project: ModelHandle, - relative_to: Option>, + currently_opened_path: Option, + history_items: Vec, cx: &mut ViewContext, ) -> Self { cx.observe(&project, |picker, _, cx| { @@ -120,10 +131,11 @@ impl FileFinderDelegate { latest_search_id: 0, latest_search_did_cancel: false, latest_search_query: None, - relative_to, + currently_opened_path, matches: Vec::new(), selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), + history_items, } } @@ -132,7 +144,10 @@ impl FileFinderDelegate { query: PathLikeWithPosition, cx: &mut ViewContext, ) -> Task<()> { - let relative_to = self.relative_to.clone(); + let relative_to = self + .currently_opened_path + .as_ref() + .map(|project_path| Arc::clone(&project_path.path)); let worktrees = self .project .read(cx) @@ -239,12 +254,22 @@ impl PickerDelegate for FileFinderDelegate { if raw_query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); self.matches.clear(); + self.matches = self - .project - .read(cx) - .search_panel_state() - .recent_selections() - .cloned() + .currently_opened_path + .iter() // if exists, bubble the currently opened path to the top + .chain(self.history_items.iter().filter(|history_item| { + Some(*history_item) != self.currently_opened_path.as_ref() + })) + .enumerate() + .map(|(i, history_item)| PathMatch { + score: i as f64, + positions: Vec::new(), + worktree_id: history_item.worktree_id.0, + path: Arc::clone(&history_item.path), + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }) .collect(); cx.notify(); Task::ready(()) @@ -268,10 +293,6 @@ impl PickerDelegate for FileFinderDelegate { fn confirm(&mut self, cx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected_index()) { if let Some(workspace) = self.workspace.upgrade(cx) { - self.project.update(cx, |project, _cx| { - project.update_search_panel_state().add_selection(m.clone()) - }); - let project_path = ProjectPath { worktree_id: WorktreeId::from_usize(m.worktree_id), path: m.path.clone(), @@ -613,6 +634,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -697,6 +719,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -732,6 +755,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -797,6 +821,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -846,13 +871,17 @@ mod tests { // When workspace has an active item, sort items which are closer to that item // first when they have the same name. In this case, b.txt is closer to dir2's a.txt // so that one should be sorted earlier - let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt"))); + let b_path = Some(ProjectPath { + worktree_id: WorktreeId(workspace.id()), + path: Arc::from(Path::new("/root/dir2/b.txt")), + }); let (_, finder) = cx.add_window(|cx| { Picker::new( FileFinderDelegate::new( workspace.downgrade(), workspace.read(cx).project().clone(), b_path, + Vec::new(), cx, ), cx, @@ -897,6 +926,7 @@ mod tests { workspace.downgrade(), workspace.read(cx).project().clone(), None, + Vec::new(), cx, ), cx, @@ -913,97 +943,6 @@ mod tests { }); } - #[gpui::test] - async fn test_query_history(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(window_id, Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches("fir".to_string(), cx) - }) - .await; - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); - - cx.dispatch_action(window_id, Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches("sec".to_string(), cx) - }) - .await; - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); - - finder.read_with(cx, |finder, cx| { - let recent_query_paths = finder - .delegate() - .project - .read(cx) - .search_panel_state() - .recent_selections() - .map(|query| query.path.to_path_buf()) - .collect::>(); - assert_eq!( - vec![ - Path::new("test/second.rs").to_path_buf(), - Path::new("test/first.rs").to_path_buf(), - ], - recent_query_paths, - "Two finder queries should produce only two recent queries. Second query should be more recent (first)" - ) - }); - - cx.dispatch_action(window_id, Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches("fir".to_string(), cx) - }) - .await; - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); - - finder.read_with(cx, |finder, cx| { - let recent_query_paths = finder - .delegate() - .project - .read(cx) - .search_panel_state() - .recent_selections() - .map(|query| query.path.to_path_buf()) - .collect::>(); - assert_eq!( - vec![ - Path::new("test/first.rs").to_path_buf(), - Path::new("test/second.rs").to_path_buf(), - ], - recent_query_paths, - "Three finder queries on two different files should produce only two recent queries. First query should be more recent (first), since got queried again" - ) - }); - } - fn init_test(cx: &mut TestAppContext) -> Arc { cx.foreground().forbid_parking(); cx.update(|cx| { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f19ef446442345c9d24706839fb0c00275cf9da1..13809622f9e3191cf4b12eaaa0347c3f98e0f050 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -12,14 +12,13 @@ mod project_tests; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; +use collections::{hash_map, BTreeMap, HashMap, HashSet}; use copilot::Copilot; use futures::{ channel::mpsc::{self, UnboundedReceiver}, future::{try_join_all, Shared}, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt, }; -use fuzzy::PathMatch; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, @@ -136,7 +135,6 @@ pub struct Project { _maintain_workspace_config: Task<()>, terminals: Terminals, copilot_enabled: bool, - search_panel_state: SearchPanelState, } struct LspBufferSnapshot { @@ -390,42 +388,6 @@ impl FormatTrigger { } } -const MAX_RECENT_SELECTIONS: usize = 20; - -#[derive(Debug, Default)] -pub struct SearchPanelState { - recent_selections: VecDeque, -} - -impl SearchPanelState { - pub fn recent_selections(&self) -> impl Iterator { - self.recent_selections.iter().rev() - } - - pub fn add_selection(&mut self, mut new_selection: PathMatch) { - let old_len = self.recent_selections.len(); - - // remove `new_selection` element, if it's in the list - self.recent_selections.retain(|old_selection| { - old_selection.worktree_id != new_selection.worktree_id - || old_selection.path != new_selection.path - }); - // if `new_selection` was not present and we're adding a new element, - // ensure we do not exceed max allowed elements - if self.recent_selections.len() == old_len { - if self.recent_selections.len() >= MAX_RECENT_SELECTIONS { - self.recent_selections.pop_front(); - } - } - - // do not highlight query matches in the selection - new_selection.positions.clear(); - // always re-add the element even if it exists to the back - // this way, it gets to the top as the most recently selected element - self.recent_selections.push_back(new_selection); - } -} - impl Project { pub fn init_settings(cx: &mut AppContext) { settings::register::(cx); @@ -525,7 +487,6 @@ impl Project { local_handles: Vec::new(), }, copilot_enabled: Copilot::global(cx).is_some(), - search_panel_state: SearchPanelState::default(), } }) } @@ -616,7 +577,6 @@ impl Project { local_handles: Vec::new(), }, copilot_enabled: Copilot::global(cx).is_some(), - search_panel_state: SearchPanelState::default(), }; for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); @@ -6475,14 +6435,6 @@ impl Project { }) } - pub fn search_panel_state(&self) -> &SearchPanelState { - &self.search_panel_state - } - - pub fn update_search_panel_state(&mut self) -> &mut SearchPanelState { - &mut self.search_panel_state - } - fn primary_language_servers_for_buffer( &self, buffer: &Buffer, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 403d8934259e864a427e690fe497e2cd87f23ef6..550a27ea9f307372533a4801ab1d637e7269a539 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -58,7 +58,7 @@ use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use util::{paths::HOME, ResultExt, TakeUntilExt, TryFutureExt}; #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] -pub struct WorktreeId(usize); +pub struct WorktreeId(pub usize); pub enum Worktree { Local(LocalWorktree), diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 26797e8d6c3a1a3635936e481229e4705d640270..33e5e7aefe0cc451efb4ad14b9639a59bd471fbf 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -38,6 +38,7 @@ theme = { path = "../theme" } util = { path = "../util" } async-recursion = "1.0.0" +itertools = "0.10" bincode = "1.2.1" anyhow.workspace = true futures.workspace = true diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index d1ec80de9598c8caa08c866c7b751ff987749419..beec6a051525b573c78872ee3c3c212047f1cda5 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -12,6 +12,7 @@ use gpui::{ platform::{CursorStyle, MouseButton}, AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle, }; +use std::sync::{atomic::AtomicUsize, Arc}; use theme::Theme; pub use toggle_dock_button::ToggleDockButton; @@ -170,13 +171,21 @@ impl Dock { pub fn new( default_item_factory: DockDefaultItemFactory, background_actions: BackgroundActions, + pane_history_timestamp: Arc, cx: &mut ViewContext, ) -> Self { let position = DockPosition::Hidden(settings::get::(cx).default_dock_anchor); let workspace = cx.weak_handle(); - let pane = - cx.add_view(|cx| Pane::new(workspace, Some(position.anchor()), background_actions, cx)); + let pane = cx.add_view(|cx| { + Pane::new( + workspace, + Some(position.anchor()), + background_actions, + pane_history_timestamp, + cx, + ) + }); pane.update(cx, |pane, cx| { pane.set_active(false, cx); }); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 200f83700b6c899d793d551f01dcf830cb9f6235..368afcd16c8f134a0c9a40dc6e0e32a192cb7c39 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -30,7 +30,17 @@ use gpui::{ }; use project::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; -use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; +use std::{ + any::Any, + cell::RefCell, + cmp, mem, + path::Path, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; use theme::Theme; use util::ResultExt; @@ -159,6 +169,8 @@ pub struct ItemNavHistory { item: Rc, } +pub struct PaneNavHistory(Rc>); + struct NavHistory { mode: NavigationMode, backward_stack: VecDeque, @@ -166,6 +178,7 @@ struct NavHistory { closed_stack: VecDeque, paths_by_item: HashMap, pane: WeakViewHandle, + next_timestamp: Arc, } #[derive(Copy, Clone)] @@ -187,6 +200,7 @@ impl Default for NavigationMode { pub struct NavigationEntry { pub item: Rc, pub data: Option>, + pub timestamp: usize, } struct DraggedItem { @@ -226,6 +240,7 @@ impl Pane { workspace: WeakViewHandle, docked: Option, background_actions: BackgroundActions, + next_timestamp: Arc, cx: &mut ViewContext, ) -> Self { let pane_view_id = cx.view_id(); @@ -249,6 +264,7 @@ impl Pane { closed_stack: Default::default(), paths_by_item: Default::default(), pane: handle.clone(), + next_timestamp, })), toolbar: cx.add_view(|_| Toolbar::new(handle)), tab_bar_context_menu: TabBarContextMenu { @@ -292,6 +308,10 @@ impl Pane { } } + pub fn nav_history(&self) -> PaneNavHistory { + PaneNavHistory(self.nav_history.clone()) + } + pub fn go_back( workspace: &mut Workspace, pane: Option>, @@ -1942,6 +1962,7 @@ impl NavHistory { self.backward_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); self.forward_stack.clear(); } @@ -1952,6 +1973,7 @@ impl NavHistory { self.forward_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } NavigationMode::GoingForward => { @@ -1961,6 +1983,7 @@ impl NavHistory { self.backward_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } NavigationMode::ClosingItem => { @@ -1970,6 +1993,7 @@ impl NavHistory { self.closed_stack.push_back(NavigationEntry { item, data: data.map(|data| Box::new(data) as Box), + timestamp: self.next_timestamp.fetch_add(1, Ordering::SeqCst), }); } } @@ -1985,6 +2009,31 @@ impl NavHistory { } } +impl PaneNavHistory { + pub fn for_each_entry( + &self, + cx: &AppContext, + mut f: impl FnMut(&NavigationEntry, ProjectPath), + ) { + let borrowed_history = self.0.borrow(); + borrowed_history + .forward_stack + .iter() + .chain(borrowed_history.backward_stack.iter()) + .chain(borrowed_history.closed_stack.iter()) + .for_each(|entry| { + if let Some(path) = borrowed_history.paths_by_item.get(&entry.item.id()) { + f(entry, path.clone()); + } else if let Some(item) = entry.item.upgrade(cx) { + let path = item.project_path(cx); + if let Some(path) = path { + f(entry, path); + } + } + }) + } +} + pub struct PaneBackdrop { child_view: usize, child: AnyElement, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8ca6358f9aeafc8397bcc3c24aa8a157503db638..28ad294798ee179952f03d5feb1b385e6ae2ac97 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -47,6 +47,7 @@ use gpui::{ WindowContext, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; +use itertools::Itertools; use language::{LanguageRegistry, Rope}; use std::{ any::TypeId, @@ -55,7 +56,7 @@ use std::{ future::Future, path::{Path, PathBuf}, str, - sync::Arc, + sync::{atomic::AtomicUsize, Arc}, time::Duration, }; @@ -481,6 +482,7 @@ pub struct Workspace { _window_subscriptions: [Subscription; 3], _apply_leader_updates: Task>, _observe_current_user: Task>, + pane_history_timestamp: Arc, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] @@ -542,15 +544,24 @@ impl Workspace { .detach(); let weak_handle = cx.weak_handle(); + let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); - let center_pane = cx - .add_view(|cx| Pane::new(weak_handle.clone(), None, app_state.background_actions, cx)); + let center_pane = cx.add_view(|cx| { + Pane::new( + weak_handle.clone(), + None, + app_state.background_actions, + pane_history_timestamp.clone(), + cx, + ) + }); cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); cx.focus(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); let dock = Dock::new( app_state.dock_default_item_factory, app_state.background_actions, + pane_history_timestamp.clone(), cx, ); let dock_pane = dock.pane().clone(); @@ -665,6 +676,7 @@ impl Workspace { _apply_leader_updates, leader_updates_tx, _window_subscriptions: subscriptions, + pane_history_timestamp, }; this.project_remote_id_changed(project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); @@ -825,6 +837,39 @@ impl Workspace { &self.project } + pub fn recent_navigation_history( + &self, + limit: Option, + cx: &AppContext, + ) -> Vec { + let mut history: HashMap = HashMap::default(); + for pane in &self.panes { + let pane = pane.read(cx); + pane.nav_history() + .for_each_entry(cx, |entry, project_path| { + let timestamp = entry.timestamp; + match history.entry(project_path) { + hash_map::Entry::Occupied(mut entry) => { + if ×tamp > entry.get() { + entry.insert(timestamp); + } + } + hash_map::Entry::Vacant(entry) => { + entry.insert(timestamp); + } + } + }); + } + + history + .into_iter() + .sorted_by_key(|(_, timestamp)| *timestamp) + .map(|(project_path, _)| project_path) + .rev() + .take(limit.unwrap_or(usize::MAX)) + .collect() + } + pub fn client(&self) -> &Client { &self.app_state.client } @@ -1386,6 +1431,7 @@ impl Workspace { self.weak_handle(), None, self.app_state.background_actions, + self.pane_history_timestamp.clone(), cx, ) });