From 711d2c6fe771e9ba7094fa68a2c4a1eb35cfa2c5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 13 May 2023 00:44:21 +0300 Subject: [PATCH 1/3] Maintain recently opened files history --- crates/file_finder/src/file_finder.rs | 104 +++++++++++++++++++++++++- crates/project/src/project.rs | 50 ++++++++++++- 2 files changed, 151 insertions(+), 3 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index b318f1d167..311882bd8e 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -239,6 +239,13 @@ 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() + .collect(); cx.notify(); Task::ready(()) } else { @@ -261,11 +268,14 @@ 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(), }; - let open_task = workspace.update(cx, |workspace, cx| { workspace.open_path(project_path.clone(), None, true, cx) }); @@ -301,7 +311,6 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - workspace .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) .log_err(); @@ -904,6 +913,97 @@ 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 13809622f9..f19ef44644 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -12,13 +12,14 @@ mod project_tests; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; 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, @@ -135,6 +136,7 @@ pub struct Project { _maintain_workspace_config: Task<()>, terminals: Terminals, copilot_enabled: bool, + search_panel_state: SearchPanelState, } struct LspBufferSnapshot { @@ -388,6 +390,42 @@ 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); @@ -487,6 +525,7 @@ impl Project { local_handles: Vec::new(), }, copilot_enabled: Copilot::global(cx).is_some(), + search_panel_state: SearchPanelState::default(), } }) } @@ -577,6 +616,7 @@ 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); @@ -6435,6 +6475,14 @@ 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, From 201d513c5019bb901e580262fae9c15505b4eda6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 May 2023 17:35:06 +0300 Subject: [PATCH 2/3] 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 ce727a9c6c..be8a8a74bf 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 311882bd8e..94b48d9e45 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 f19ef44644..13809622f9 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 403d893425..550a27ea9f 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 26797e8d6c..33e5e7aefe 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 d1ec80de95..beec6a0515 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 200f83700b..368afcd16c 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 8ca6358f9a..28ad294798 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, ) }); From 2ec994dfcd77980d2ec62ff8aea5697f0ed070e5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 18 May 2023 16:33:43 +0300 Subject: [PATCH 3/3] Add a unit test --- crates/file_finder/src/file_finder.rs | 250 +++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 4 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 94b48d9e45..8ba58819a3 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -373,14 +373,14 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(test)] mod tests { - use std::time::Duration; + use std::{assert_eq, collections::HashMap, time::Duration}; use super::*; use editor::Editor; - use gpui::TestAppContext; + use gpui::{TestAppContext, ViewHandle}; use menu::{Confirm, SelectNext}; use serde_json::json; - use workspace::{AppState, Workspace}; + use workspace::{AppState, Pane, Workspace}; #[ctor::ctor] fn init_logger() { @@ -867,12 +867,17 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId(worktrees[0].id()) + }); // 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(ProjectPath { - worktree_id: WorktreeId(workspace.id()), + worktree_id, path: Arc::from(Path::new("/root/dir2/b.txt")), }); let (_, finder) = cx.add_window(|cx| { @@ -943,6 +948,243 @@ mod tests { }); } + #[gpui::test] + async fn test_query_history( + deterministic: Arc, + 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)); + let worktree_id = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + WorktreeId(worktrees[0].id()) + }); + + // Open and close panels, getting their history items afterwards. + // Ensure history items get populated with opened items, and items are kept in a certain order. + // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. + // + // TODO: without closing, the opened items do not propagate their history changes for some reason + // it does work in real app though, only tests do not propagate. + + let initial_history = open_close_queried_buffer( + "fir", + 1, + "first.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert!( + initial_history.is_empty(), + "Should have no history before opening any files" + ); + + let history_after_first = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_first, + vec![ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }], + "Should show 1st opened item in the history when opening the 2nd item" + ); + + let history_after_second = open_close_queried_buffer( + "thi", + 1, + "third.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_second, + vec![ + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + ], + "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ +2nd item should be the first in the history, as the last opened." + ); + + let history_after_third = open_close_queried_buffer( + "sec", + 1, + "second.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_third, + vec![ + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ +3rd item should be the first in the history, as the last opened." + ); + + let history_after_second_again = open_close_queried_buffer( + "thi", + 1, + "third.rs", + window_id, + &workspace, + &deterministic, + cx, + ) + .await; + assert_eq!( + history_after_second_again, + vec![ + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/second.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/third.rs")), + }, + ProjectPath { + worktree_id, + path: Arc::from(Path::new("test/first.rs")), + }, + ], + "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ +2nd item, as the last opened, 3rd item should go next as it was opened right before." + ); + } + + async fn open_close_queried_buffer( + input: &str, + expected_matches: usize, + expected_editor_title: &str, + window_id: usize, + workspace: &ViewHandle, + deterministic: &gpui::executor::Deterministic, + cx: &mut gpui::TestAppContext, + ) -> Vec { + 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(input.to_string(), cx) + }) + .await; + let history_items = finder.read_with(cx, |finder, _| { + assert_eq!( + finder.delegate().matches.len(), + expected_matches, + "Unexpected number of matches found for query {input}" + ); + finder.delegate().history_items.clone() + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(window_id, SelectNext); + cx.dispatch_action(window_id, Confirm); + deterministic.run_until_parked(); + active_pane + .condition(cx, |pane, _| pane.active_item().is_some()) + .await; + cx.read(|cx| { + let active_item = active_pane.read(cx).active_item().unwrap(); + let active_editor_title = active_item + .as_any() + .downcast_ref::() + .unwrap() + .read(cx) + .title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + let mut original_items = HashMap::new(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.id(); + let pane = pane.read(cx); + let insertion_result = original_items.insert(pane_id, pane.items().count()); + assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); + } + }); + workspace.update(cx, |workspace, cx| { + Pane::close_active_item(workspace, &workspace::CloseActiveItem, cx); + }); + deterministic.run_until_parked(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.id(); + let pane = pane.read(cx); + match original_items.remove(&pane_id) { + Some(original_items) => { + assert_eq!( + pane.items().count(), + original_items.saturating_sub(1), + "Pane id {pane_id} should have item closed" + ); + } + None => panic!("Pane id {pane_id} not found in original items"), + } + } + }); + + history_items + } + fn init_test(cx: &mut TestAppContext) -> Arc { cx.foreground().forbid_parking(); cx.update(|cx| {