diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 67fb1e400f..13296887cb 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,7 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, AppContext, Div, EventEmitter, Render, Task, View, ViewContext, WindowContext, + actions, div, AppContext, Component, Div, EventEmitter, Model, ParentElement, Render, + StatelessInteractive, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -14,7 +15,9 @@ use std::{ }, }; use text::Point; -use util::{paths::PathLikeWithPosition, post_inc}; +use theme::ActiveTheme; +use ui::{v_stack, HighlightedLabel, StyledExt}; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); @@ -24,14 +27,16 @@ pub struct FileFinder { } pub fn init(cx: &mut AppContext) { - cx.observe_new_views(FileFinder::register); + cx.observe_new_views(FileFinder::register).detach(); } impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { + dbg!("yay"); workspace.register_action(|workspace, _: &Toggle, cx| { + dbg!("yayer"); let Some(file_finder) = workspace.current_modal::(cx) else { - workspace.toggle_modal(cx, |cx| FileFinder::new(workspace, cx)); + Self::open(workspace, cx); return; }; file_finder.update(cx, |file_finder, cx| { @@ -42,7 +47,7 @@ impl FileFinder { }); } - fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> Self { + fn open(workspace: &mut Workspace, cx: &mut ViewContext) { let project = workspace.project().read(cx); let currently_opened_path = workspace @@ -84,20 +89,25 @@ impl FileFinder { .collect(); let project = workspace.project().clone(); - let workspace = cx.handle().downgrade(); - let finder = cx.add_view(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace, - project, - currently_opened_path, - history_items, - cx, - ), + let weak_workspace = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = FileFinderDelegate::new( + cx.view().downgrade(), + weak_workspace, + project, + currently_opened_path, + history_items, cx, - ) + ); + + FileFinder::new(delegate, cx) }); - finder + } + + fn new(delegate: FileFinderDelegate, cx: &mut ViewContext) -> Self { + Self { + picker: cx.build_view(|cx| Picker::new(delegate, cx)), + } } } @@ -116,8 +126,9 @@ impl Render for FileFinder { } pub struct FileFinderDelegate { - workspace: WeakViewHandle, - project: ModelHandle, + file_finder: WeakView, + workspace: WeakView, + project: Model, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, @@ -263,82 +274,6 @@ impl FoundPath { const MAX_RECENT_SELECTIONS: usize = 20; -fn toggle_or_cycle_file_finder( - workspace: &mut Workspace, - _: &Toggle, - cx: &mut ViewContext, -) { - match workspace.modal::() { - Some(file_finder) => file_finder.update(cx, |file_finder, cx| { - let current_index = file_finder.delegate().selected_index(); - file_finder.select_next(&menu::SelectNext, cx); - let new_index = file_finder.delegate().selected_index(); - if current_index == new_index { - file_finder.select_first(&menu::SelectFirst, cx); - } - }), - None => { - workspace.toggle_modal(cx, |workspace, cx| { - let project = workspace.project().read(cx); - - let currently_opened_path = workspace - .active_item(cx) - .and_then(|item| item.project_path(cx)) - .map(|project_path| { - let abs_path = project - .worktree_for_id(project_path.worktree_id, cx) - .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); - FoundPath::new(project_path, abs_path) - }); - - // if exists, bubble the currently opened path to the top - let history_items = currently_opened_path - .clone() - .into_iter() - .chain( - workspace - .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) - .into_iter() - .filter(|(history_path, _)| { - Some(history_path) - != currently_opened_path - .as_ref() - .map(|found_path| &found_path.project) - }) - .filter(|(_, history_abs_path)| { - history_abs_path.as_ref() - != currently_opened_path - .as_ref() - .and_then(|found_path| found_path.absolute.as_ref()) - }) - .filter(|(_, history_abs_path)| match history_abs_path { - Some(abs_path) => history_file_exists(abs_path), - None => true, - }) - .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), - ) - .collect(); - - let project = workspace.project().clone(); - let workspace = cx.handle().downgrade(); - let finder = cx.add_view(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace, - project, - currently_opened_path, - history_items, - cx, - ), - cx, - ) - }); - finder - }); - } - } -} - #[cfg(not(test))] fn history_file_exists(abs_path: &PathBuf) -> bool { abs_path.exists() @@ -371,17 +306,23 @@ impl FileSearchQuery { impl FileFinderDelegate { fn new( - workspace: WeakViewHandle, - project: ModelHandle, + file_finder: WeakView, + workspace: WeakView, + project: Model, currently_opened_path: Option, history_items: Vec, cx: &mut ViewContext, ) -> Self { - cx.observe(&project, |picker, _, cx| { - picker.update_matches(picker.query(cx), cx); + cx.observe(&project, |file_finder, _, cx| { + //todo!() We should probably not re-render on every project anything + file_finder + .picker + .update(cx, |picker, cx| picker.refresh(cx)) }) .detach(); + Self { + file_finder, workspace, project, search_count: 0, @@ -399,7 +340,7 @@ impl FileFinderDelegate { fn spawn_search( &mut self, query: PathLikeWithPosition, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) -> Task<()> { let relative_to = self .currently_opened_path @@ -437,14 +378,14 @@ impl FileFinderDelegate { false, 100, &cancel_flag, - cx.background(), + cx.background_executor().clone(), ) .await; let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); picker .update(&mut cx, |picker, cx| { picker - .delegate_mut() + .delegate .set_search_matches(search_id, did_cancel, query, matches, cx) }) .log_err(); @@ -457,7 +398,7 @@ impl FileFinderDelegate { did_cancel: bool, query: PathLikeWithPosition, matches: Vec, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) { if search_id >= self.latest_search_id { self.latest_search_id = search_id; @@ -589,6 +530,8 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { + type ListItem = Div>; + fn placeholder_text(&self) -> Arc { "Search project files...".into() } @@ -601,12 +544,16 @@ impl PickerDelegate for FileFinderDelegate { self.selected_index.unwrap_or(0) } - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { self.selected_index = Some(ix); cx.notify(); } - fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext) -> Task<()> { + fn update_matches( + &mut self, + raw_query: String, + cx: &mut ViewContext>, + ) -> Task<()> { if raw_query.is_empty() { let project = self.project.read(cx); self.latest_search_id = post_inc(&mut self.search_count); @@ -644,9 +591,9 @@ impl PickerDelegate for FileFinderDelegate { } } - fn confirm(&mut self, secondary: bool, cx: &mut ViewContext) { + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { if let Some(m) = self.matches.get(self.selected_index()) { - if let Some(workspace) = self.workspace.upgrade(cx) { + if let Some(workspace) = self.workspace.upgrade() { let open_task = workspace.update(cx, move |workspace, cx| { let split_or_open = |workspace: &mut Workspace, project_path, cx| { if secondary { @@ -722,6 +669,8 @@ impl PickerDelegate for FileFinderDelegate { .and_then(|query| query.column) .unwrap_or(0) .saturating_sub(1); + let finder = self.file_finder.clone(); + cx.spawn(|_, mut cx| async move { let item = open_task.await.log_err()?; if let Some(row) = row { @@ -740,10 +689,9 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } - workspace - .downgrade() - .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) - .log_err(); + finder + .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .ok()?; Some(()) }) @@ -752,1490 +700,1497 @@ impl PickerDelegate for FileFinderDelegate { } } - fn dismissed(&mut self, _: &mut ViewContext) {} + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.file_finder + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &AppContext, - ) -> AnyElement> { + cx: &mut ViewContext>, + ) -> Self::ListItem { let path_match = self .matches .get(ix) .expect("Invalid matches state: no element for index {ix}"); - let theme = theme::current(cx); - let style = theme.picker.item.in_state(selected).style_for(mouse_state); + let theme = cx.theme(); + let colors = theme.colors(); + let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); - Flex::column() - .with_child( - Label::new(file_name, style.label.clone()).with_highlights(file_name_positions), + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child( + v_stack() + .child(HighlightedLabel::new(file_name, file_name_positions)) + .child(HighlightedLabel::new(full_path, full_path_positions)), ) - .with_child( - Label::new(full_path, style.label.clone()).with_highlights(full_path_positions), - ) - .flex(1., false) - .contained() - .with_style(style.container) - .into_any_named("match") } } -#[cfg(test)] -mod tests { - use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - - use super::*; - use editor::Editor; - use gpui::{TestAppContext, ViewHandle}; - use menu::{Confirm, SelectNext}; - use serde_json::json; - use workspace::{AppState, Workspace}; - - #[ctor::ctor] - fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } - } - - #[gpui::test] - async fn test_matching_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": { - "banana": "", - "bandana": "", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - cx.dispatch_action(window.into(), Toggle); - - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches("bna".to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - assert_eq!(finder.delegate().matches.len(), 2); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.into(), SelectNext); - cx.dispatch_action(window.into(), Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - cx.read(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - assert_eq!( - active_item - .as_any() - .downcast_ref::() - .unwrap() - .read(cx) - .title(cx), - "bandana" - ); - }); - } - - #[gpui::test] - async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let first_file_name = "first.rs"; - let first_file_contents = "// First Rust file"; - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - first_file_name: first_file_contents, - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - cx.dispatch_action(window.into(), Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - - let file_query = &first_file_name[..3]; - let file_row = 1; - let file_column = 3; - assert!(file_column <= first_file_contents.len()); - let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(query_inside_file.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let finder = finder.delegate(); - assert_eq!(finder.matches.len(), 1); - let latest_search_query = finder - .latest_search_query - .as_ref() - .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); - assert_eq!( - latest_search_query.path_like.file_query_end, - Some(file_query.len()) - ); - assert_eq!(latest_search_query.row, Some(file_row)); - assert_eq!(latest_search_query.column, Some(file_column as u32)); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.into(), SelectNext); - cx.dispatch_action(window.into(), Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - let editor = cx.update(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - active_item.downcast::().unwrap() - }); - cx.foreground().advance_clock(Duration::from_secs(2)); - cx.foreground().start_waiting(); - cx.foreground().finish_waiting(); - editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); - assert_eq!( - all_selections.len(), - 1, - "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" - ); - let caret_selection = all_selections.into_iter().next().unwrap(); - assert_eq!(caret_selection.start, caret_selection.end, - "Caret selection should have its start and end at the same position"); - assert_eq!(file_row, caret_selection.start.row + 1, - "Query inside file should get caret with the same focus row"); - assert_eq!(file_column, caret_selection.start.column as usize + 1, - "Query inside file should get caret with the same focus column"); - }); - } - - #[gpui::test] - async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let first_file_name = "first.rs"; - let first_file_contents = "// First Rust file"; - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "test": { - first_file_name: first_file_contents, - "second.rs": "// Second Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - cx.dispatch_action(window.into(), Toggle); - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - - let file_query = &first_file_name[..3]; - let file_row = 200; - let file_column = 300; - assert!(file_column > first_file_contents.len()); - let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(query_outside_file.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let finder = finder.delegate(); - assert_eq!(finder.matches.len(), 1); - let latest_search_query = finder - .latest_search_query - .as_ref() - .expect("Finder should have a query after the update_matches call"); - assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); - assert_eq!( - latest_search_query.path_like.file_query_end, - Some(file_query.len()) - ); - assert_eq!(latest_search_query.row, Some(file_row)); - assert_eq!(latest_search_query.column, Some(file_column as u32)); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.into(), SelectNext); - cx.dispatch_action(window.into(), Confirm); - active_pane - .condition(cx, |pane, _| pane.active_item().is_some()) - .await; - let editor = cx.update(|cx| { - let active_item = active_pane.read(cx).active_item().unwrap(); - active_item.downcast::().unwrap() - }); - cx.foreground().advance_clock(Duration::from_secs(2)); - cx.foreground().start_waiting(); - cx.foreground().finish_waiting(); - editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); - assert_eq!( - all_selections.len(), - 1, - "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" - ); - let caret_selection = all_selections.into_iter().next().unwrap(); - assert_eq!(caret_selection.start, caret_selection.end, - "Caret selection should have its start and end at the same position"); - assert_eq!(0, caret_selection.start.row, - "Excessive rows (as in query outside file borders) should get trimmed to last file row"); - assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, - "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); - }); - } - - #[gpui::test] - async fn test_matching_cancellation(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/dir", - json!({ - "hello": "", - "goodbye": "", - "halogen-light": "", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - - let query = test_path_like("hi"); - finder - .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) - .await; - finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); - - finder.update(cx, |finder, cx| { - let delegate = finder.delegate_mut(); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - - // Simulate a search being cancelled after the time limit, - // returning only a subset of the matches that would have been found. - drop(delegate.spawn_search(query.clone(), cx)); - delegate.set_search_matches( - delegate.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[1].clone(), matches[3].clone()], - cx, - ); - - // Simulate another cancellation. - drop(delegate.spawn_search(query.clone(), cx)); - delegate.set_search_matches( - delegate.latest_search_id, - true, // did-cancel - query.clone(), - vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], - cx, - ); - - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); - }); - } - - #[gpui::test] - async fn test_ignored_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/ancestor", - json!({ - ".gitignore": "ignored-root", - "ignored-root": { - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }, - "tracked-root": { - ".gitignore": "height", - "happiness": "", - "height": "", - "hi": "", - "hiccup": "", - }, - }), - ) - .await; - - let project = Project::test( - app_state.fs.clone(), - [ - "/ancestor/tracked-root".as_ref(), - "/ancestor/ignored-root".as_ref(), - ], - cx, - ) - .await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("hi"), cx) - }) - .await; - finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); - } - - #[gpui::test] - async fn test_single_file_worktrees(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) - .await; - - let project = Project::test( - app_state.fs.clone(), - ["/root/the-parent-dir/the-file".as_ref()], - cx, - ) - .await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - - // Even though there is only one worktree, that worktree's filename - // is included in the matching, because the worktree is a single file. - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("thf"), cx) - }) - .await; - cx.read(|cx| { - let finder = finder.read(cx); - let delegate = finder.delegate(); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - assert_eq!(matches.len(), 1); - - let (file_name, file_name_positions, full_path, full_path_positions) = - delegate.labels_for_path_match(&matches[0]); - assert_eq!(file_name, "the-file"); - assert_eq!(file_name_positions, &[0, 1, 4]); - assert_eq!(full_path, "the-file"); - assert_eq!(full_path_positions, &[0, 1, 4]); - }); - - // Since the worktree root is a file, searching for its name followed by a slash does - // not match anything. - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("thf/"), cx) - }) - .await; - finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); - } - - #[gpui::test] - async fn test_path_distance_ordering(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "dir1": { "a.txt": "" }, - "dir2": { - "a.txt": "", - "b.txt": "" - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - WorktreeId::from_usize(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(dummy_found_path(ProjectPath { - worktree_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, - ) - }) - .root(cx); - - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) - }) - .await; - - finder.read_with(cx, |f, _| { - let delegate = f.delegate(); - assert!( - delegate.matches.history.is_empty(), - "Search matches expected" - ); - let matches = delegate.matches.search.clone(); - assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); - assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); - }); - } - - #[gpui::test] - async fn test_search_worktree_without_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "dir1": {}, - "dir2": { - "dir3": {} - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); - let finder = cx - .add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }) - .root(cx); - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("dir"), cx) - }) - .await; - cx.read(|cx| { - let finder = finder.read(cx); - assert_eq!(finder.delegate().matches.len(), 0); - }); - } - - #[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 = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - WorktreeId::from_usize(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.into(), - &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.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_first, - vec![FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/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.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_second, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/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.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_third, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/third.rs")), - }, - Some(PathBuf::from("/src/test/third.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/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.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - history_after_second_again, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/third.rs")), - }, - Some(PathBuf::from("/src/test/third.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/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." - ); - } - - #[gpui::test] - async fn test_external_files_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", - } - }), - ) - .await; - - app_state - .fs - .as_fake() - .insert_tree( - "/external-src", - json!({ - "test": { - "third.rs": "// Third Rust file", - "fourth.rs": "// Fourth Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - cx.update(|cx| { - project.update(cx, |project, cx| { - project.find_or_create_local_worktree("/external-src", false, cx) - }) - }) - .detach(); - deterministic.run_until_parked(); - - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1,); - - WorktreeId::from_usize(worktrees[0].id()) - }); - workspace - .update(cx, |workspace, cx| { - workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) - }) - .detach(); - deterministic.run_until_parked(); - let external_worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!( - worktrees.len(), - 2, - "External file should get opened in a new worktree" - ); - - WorktreeId::from_usize( - worktrees - .into_iter() - .find(|worktree| worktree.id() != worktree_id.to_usize()) - .expect("New worktree should have a different id") - .id(), - ) - }); - close_active_item(&workspace, &deterministic, cx).await; - - let initial_history_items = open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - initial_history_items, - vec![FoundPath::new( - ProjectPath { - worktree_id: external_worktree_id, - path: Arc::from(Path::new("")), - }, - Some(PathBuf::from("/external-src/test/third.rs")) - )], - "Should show external file with its full path in the history after it was open" - ); - - let updated_history_items = open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - assert_eq!( - updated_history_items, - vec![ - FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/second.rs")), - }, - Some(PathBuf::from("/src/test/second.rs")) - ), - FoundPath::new( - ProjectPath { - worktree_id: external_worktree_id, - path: Arc::from(Path::new("")), - }, - Some(PathBuf::from("/external-src/test/third.rs")) - ), - ], - "Should keep external file with history updates", - ); - } - - #[gpui::test] - async fn test_toggle_panel_new_selections( - 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 = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - let current_history = open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - for expected_selected_index in 0..current_history.len() { - cx.dispatch_action(window.into(), Toggle); - let selected_index = cx.read(|cx| { - workspace - .read(cx) - .modal::() - .unwrap() - .read(cx) - .delegate() - .selected_index() - }); - assert_eq!( - selected_index, expected_selected_index, - "Should select the next item in the history" - ); - } - - cx.dispatch_action(window.into(), Toggle); - let selected_index = cx.read(|cx| { - workspace - .read(cx) - .modal::() - .unwrap() - .read(cx) - .delegate() - .selected_index() - }); - assert_eq!( - selected_index, 0, - "Should wrap around the history and start all over" - ); - } - - #[gpui::test] - async fn test_search_preserves_history_items( - 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", - "fourth.rs": "// Fourth Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - let worktree_id = cx.read(|cx| { - let worktrees = workspace.read(cx).worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1,); - - WorktreeId::from_usize(worktrees[0].id()) - }); - - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - cx.dispatch_action(window.into(), Toggle); - let first_query = "f"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(first_query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); - let history_match = delegate.matches.history.first().unwrap(); - assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); - assert_eq!(history_match.0, FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - )); - assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); - assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); - }); - - let second_query = "fsdasdsa"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(second_query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert!( - delegate.matches.history.is_empty(), - "No history entries should match {second_query}" - ); - assert!( - delegate.matches.search.is_empty(), - "No search entries should match {second_query}" - ); - }); - - let first_query_again = first_query; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder - .delegate_mut() - .update_matches(first_query_again.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); - let history_match = delegate.matches.history.first().unwrap(); - assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); - assert_eq!(history_match.0, FoundPath::new( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("test/first.rs")), - }, - Some(PathBuf::from("/src/test/first.rs")) - )); - assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); - assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); - }); - } - - #[gpui::test] - async fn test_history_items_vs_very_good_external_match( - deterministic: Arc, - cx: &mut gpui::TestAppContext, - ) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - "/src", - json!({ - "collab_ui": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - "collab_ui.rs": "// Fourth Rust file", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "sec", - 1, - "second.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - cx.dispatch_action(window.into(), Toggle); - let query = "collab_ui"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches(query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - assert!( - delegate.matches.history.is_empty(), - "History items should not math query {query}, they should be matched by name only" - ); - - let search_entries = delegate - .matches - .search - .iter() - .map(|path_match| path_match.path.to_path_buf()) - .collect::>(); - assert_eq!( - search_entries, - vec![ - PathBuf::from("collab_ui/collab_ui.rs"), - PathBuf::from("collab_ui/third.rs"), - PathBuf::from("collab_ui/first.rs"), - PathBuf::from("collab_ui/second.rs"), - ], - "Despite all search results having the same directory name, the most matching one should be on top" - ); - }); - } - - #[gpui::test] - async fn test_nonexistent_history_items_not_shown( - 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", - "nonexistent.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 = cx.add_window(|cx| Workspace::test_new(project, cx)); - let workspace = window.root(cx); - // generate some history to select from - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "non", - 1, - "nonexistent.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "thi", - 1, - "third.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - open_close_queried_buffer( - "fir", - 1, - "first.rs", - window.into(), - &workspace, - &deterministic, - cx, - ) - .await; - - cx.dispatch_action(window.into(), Toggle); - let query = "rs"; - let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - finder - .update(cx, |finder, cx| { - finder.delegate_mut().update_matches(query.to_string(), cx) - }) - .await; - finder.read_with(cx, |finder, _| { - let delegate = finder.delegate(); - let history_entries = delegate - .matches - .history - .iter() - .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) - .collect::>(); - assert_eq!( - history_entries, - vec![ - PathBuf::from("test/first.rs"), - PathBuf::from("test/third.rs"), - ], - "Should have all opened files in the history, except the ones that do not exist on disk" - ); - }); - } - - async fn open_close_queried_buffer( - input: &str, - expected_matches: usize, - expected_editor_title: &str, - window: gpui::AnyWindowHandle, - workspace: &ViewHandle, - deterministic: &gpui::executor::Deterministic, - cx: &mut gpui::TestAppContext, - ) -> Vec { - cx.dispatch_action(window, 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, SelectNext); - cx.dispatch_action(window, 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}" - ); - }); - - close_active_item(workspace, deterministic, cx).await; - - history_items - } - - async fn close_active_item( - workspace: &ViewHandle, - deterministic: &gpui::executor::Deterministic, - cx: &mut TestAppContext, - ) { - 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"); - } - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - active_pane - .update(cx, |pane, cx| { - pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) - .unwrap() - }) - .await - .unwrap(); - 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"), - } - } - }); - assert!( - original_items.len() <= 1, - "At most one panel should got closed" - ); - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let state = AppState::test(cx); - theme::init((), cx); - language::init(cx); - super::init(cx); - editor::init(cx); - workspace::init_settings(cx); - Project::init_settings(cx); - state - }) - } - - fn test_path_like(test_str: &str) -> PathLikeWithPosition { - PathLikeWithPosition::parse_str(test_str, |path_like_str| { - Ok::<_, std::convert::Infallible>(FileSearchQuery { - raw_query: test_str.to_owned(), - file_query_end: if path_like_str == test_str { - None - } else { - Some(path_like_str.len()) - }, - }) - }) - .unwrap() - } - - fn dummy_found_path(project_path: ProjectPath) -> FoundPath { - FoundPath { - project: project_path, - absolute: None, - } - } -} +// #[cfg(test)] +// mod tests { +// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + +// use super::*; +// use editor::Editor; +// use gpui::{TestAppContext, ViewHandle}; +// use menu::{Confirm, SelectNext}; +// use serde_json::json; +// use workspace::{AppState, Workspace}; + +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// async fn test_matching_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "banana": "", +// "bandana": "", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// cx.dispatch_action(window.into(), Toggle); + +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches("bna".to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// assert_eq!(finder.delegate().matches.len(), 2); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window.into(), SelectNext); +// cx.dispatch_action(window.into(), Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// assert_eq!( +// active_item +// .as_any() +// .downcast_ref::() +// .unwrap() +// .read(cx) +// .title(cx), +// "bandana" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// cx.dispatch_action(window.into(), Toggle); +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + +// let file_query = &first_file_name[..3]; +// let file_row = 1; +// let file_column = 3; +// assert!(file_column <= first_file_contents.len()); +// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(query_inside_file.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let finder = finder.delegate(); +// assert_eq!(finder.matches.len(), 1); +// let latest_search_query = finder +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window.into(), SelectNext); +// cx.dispatch_action(window.into(), Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.foreground().advance_clock(Duration::from_secs(2)); +// cx.foreground().start_waiting(); +// cx.foreground().finish_waiting(); +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(file_row, caret_selection.start.row + 1, +// "Query inside file should get caret with the same focus row"); +// assert_eq!(file_column, caret_selection.start.column as usize + 1, +// "Query inside file should get caret with the same focus column"); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// cx.dispatch_action(window.into(), Toggle); +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + +// let file_query = &first_file_name[..3]; +// let file_row = 200; +// let file_column = 300; +// assert!(file_column > first_file_contents.len()); +// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(query_outside_file.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let finder = finder.delegate(); +// assert_eq!(finder.matches.len(), 1); +// let latest_search_query = finder +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(window.into(), SelectNext); +// cx.dispatch_action(window.into(), Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.foreground().advance_clock(Duration::from_secs(2)); +// cx.foreground().start_waiting(); +// cx.foreground().finish_waiting(); +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(0, caret_selection.start.row, +// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); +// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, +// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); +// }); +// } + +// #[gpui::test] +// async fn test_matching_cancellation(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/dir", +// json!({ +// "hello": "", +// "goodbye": "", +// "halogen-light": "", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// let query = test_path_like("hi"); +// finder +// .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) +// .await; +// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5)); + +// finder.update(cx, |finder, cx| { +// let delegate = finder.delegate_mut(); +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); + +// // Simulate a search being cancelled after the time limit, +// // returning only a subset of the matches that would have been found. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[1].clone(), matches[3].clone()], +// cx, +// ); + +// // Simulate another cancellation. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], +// cx, +// ); + +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); +// }); +// } + +// #[gpui::test] +// async fn test_ignored_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/ancestor", +// json!({ +// ".gitignore": "ignored-root", +// "ignored-root": { +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// "tracked-root": { +// ".gitignore": "height", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// }), +// ) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// [ +// "/ancestor/tracked-root".as_ref(), +// "/ancestor/ignored-root".as_ref(), +// ], +// cx, +// ) +// .await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("hi"), cx) +// }) +// .await; +// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); +// } + +// #[gpui::test] +// async fn test_single_file_worktrees(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// ["/root/the-parent-dir/the-file".as_ref()], +// cx, +// ) +// .await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// // Even though there is only one worktree, that worktree's filename +// // is included in the matching, because the worktree is a single file. +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("thf"), cx) +// }) +// .await; +// cx.read(|cx| { +// let finder = finder.read(cx); +// let delegate = finder.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches.len(), 1); + +// let (file_name, file_name_positions, full_path, full_path_positions) = +// delegate.labels_for_path_match(&matches[0]); +// assert_eq!(file_name, "the-file"); +// assert_eq!(file_name_positions, &[0, 1, 4]); +// assert_eq!(full_path, "the-file"); +// assert_eq!(full_path_positions, &[0, 1, 4]); +// }); + +// // Since the worktree root is a file, searching for its name followed by a slash does +// // not match anything. +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("thf/"), cx) +// }) +// .await; +// finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); +// } + +// #[gpui::test] +// async fn test_path_distance_ordering(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": { "a.txt": "" }, +// "dir2": { +// "a.txt": "", +// "b.txt": "" +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(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(dummy_found_path(ProjectPath { +// worktree_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, +// ) +// }) +// .root(cx); + +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("a.txt"), cx) +// }) +// .await; + +// finder.read_with(cx, |f, _| { +// let delegate = f.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); +// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); +// }); +// } + +// #[gpui::test] +// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": {}, +// "dir2": { +// "dir3": {} +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); +// finder +// .update(cx, |f, cx| { +// f.delegate_mut().spawn_search(test_path_like("dir"), cx) +// }) +// .await; +// cx.read(|cx| { +// let finder = finder.read(cx); +// assert_eq!(finder.delegate().matches.len(), 0); +// }); +// } + +// #[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 = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(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.into(), +// &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.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_first, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/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.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_second, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/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.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_third, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/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.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// history_after_second_again, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/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." +// ); +// } + +// #[gpui::test] +// async fn test_external_files_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", +// } +// }), +// ) +// .await; + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/external-src", +// json!({ +// "test": { +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// cx.update(|cx| { +// project.update(cx, |project, cx| { +// project.find_or_create_local_worktree("/external-src", false, cx) +// }) +// }) +// .detach(); +// deterministic.run_until_parked(); + +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].id()) +// }); +// workspace +// .update(cx, |workspace, cx| { +// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) +// }) +// .detach(); +// deterministic.run_until_parked(); +// let external_worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!( +// worktrees.len(), +// 2, +// "External file should get opened in a new worktree" +// ); + +// WorktreeId::from_usize( +// worktrees +// .into_iter() +// .find(|worktree| worktree.id() != worktree_id.to_usize()) +// .expect("New worktree should have a different id") +// .id(), +// ) +// }); +// close_active_item(&workspace, &deterministic, cx).await; + +// let initial_history_items = open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// initial_history_items, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// )], +// "Should show external file with its full path in the history after it was open" +// ); + +// let updated_history_items = open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// assert_eq!( +// updated_history_items, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// ), +// ], +// "Should keep external file with history updates", +// ); +// } + +// #[gpui::test] +// async fn test_toggle_panel_new_selections( +// 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 = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); + +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// let current_history = open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// for expected_selected_index in 0..current_history.len() { +// cx.dispatch_action(window.into(), Toggle); +// let selected_index = cx.read(|cx| { +// workspace +// .read(cx) +// .modal::() +// .unwrap() +// .read(cx) +// .delegate() +// .selected_index() +// }); +// assert_eq!( +// selected_index, expected_selected_index, +// "Should select the next item in the history" +// ); +// } + +// cx.dispatch_action(window.into(), Toggle); +// let selected_index = cx.read(|cx| { +// workspace +// .read(cx) +// .modal::() +// .unwrap() +// .read(cx) +// .delegate() +// .selected_index() +// }); +// assert_eq!( +// selected_index, 0, +// "Should wrap around the history and start all over" +// ); +// } + +// #[gpui::test] +// async fn test_search_preserves_history_items( +// 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", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// cx.dispatch_action(window.into(), Toggle); +// let first_query = "f"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(first_query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); + +// let second_query = "fsdasdsa"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(second_query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "No history entries should match {second_query}" +// ); +// assert!( +// delegate.matches.search.is_empty(), +// "No search entries should match {second_query}" +// ); +// }); + +// let first_query_again = first_query; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate_mut() +// .update_matches(first_query_again.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); +// } + +// #[gpui::test] +// async fn test_history_items_vs_very_good_external_match( +// deterministic: Arc, +// cx: &mut gpui::TestAppContext, +// ) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "collab_ui": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "collab_ui.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "sec", +// 1, +// "second.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// cx.dispatch_action(window.into(), Toggle); +// let query = "collab_ui"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches(query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// assert!( +// delegate.matches.history.is_empty(), +// "History items should not math query {query}, they should be matched by name only" +// ); + +// let search_entries = delegate +// .matches +// .search +// .iter() +// .map(|path_match| path_match.path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// search_entries, +// vec![ +// PathBuf::from("collab_ui/collab_ui.rs"), +// PathBuf::from("collab_ui/third.rs"), +// PathBuf::from("collab_ui/first.rs"), +// PathBuf::from("collab_ui/second.rs"), +// ], +// "Despite all search results having the same directory name, the most matching one should be on top" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_nonexistent_history_items_not_shown( +// 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", +// "nonexistent.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 = cx.add_window(|cx| Workspace::test_new(project, cx)); +// let workspace = window.root(cx); +// // generate some history to select from +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "non", +// 1, +// "nonexistent.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "thi", +// 1, +// "third.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; +// open_close_queried_buffer( +// "fir", +// 1, +// "first.rs", +// window.into(), +// &workspace, +// &deterministic, +// cx, +// ) +// .await; + +// cx.dispatch_action(window.into(), Toggle); +// let query = "rs"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate_mut().update_matches(query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = finder.delegate(); +// let history_entries = delegate +// .matches +// .history +// .iter() +// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// history_entries, +// vec![ +// PathBuf::from("test/first.rs"), +// PathBuf::from("test/third.rs"), +// ], +// "Should have all opened files in the history, except the ones that do not exist on disk" +// ); +// }); +// } + +// async fn open_close_queried_buffer( +// input: &str, +// expected_matches: usize, +// expected_editor_title: &str, +// window: gpui::AnyWindowHandle, +// workspace: &ViewHandle, +// deterministic: &gpui::executor::Deterministic, +// cx: &mut gpui::TestAppContext, +// ) -> Vec { +// cx.dispatch_action(window, 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, SelectNext); +// cx.dispatch_action(window, 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}" +// ); +// }); + +// close_active_item(workspace, deterministic, cx).await; + +// history_items +// } + +// async fn close_active_item( +// workspace: &ViewHandle, +// deterministic: &gpui::executor::Deterministic, +// cx: &mut TestAppContext, +// ) { +// 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"); +// } +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// active_pane +// .update(cx, |pane, cx| { +// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) +// .unwrap() +// }) +// .await +// .unwrap(); +// 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"), +// } +// } +// }); +// assert!( +// original_items.len() <= 1, +// "At most one panel should got closed" +// ); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.foreground_executor().forbid_parking(); +// cx.update(|cx| { +// let state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// super::init(cx); +// editor::init(cx); +// workspace::init_settings(cx); +// Project::init_settings(cx); +// state +// }) +// } + +// fn test_path_like(test_str: &str) -> PathLikeWithPosition { +// PathLikeWithPosition::parse_str(test_str, |path_like_str| { +// Ok::<_, std::convert::Infallible>(FileSearchQuery { +// raw_query: test_str.to_owned(), +// file_query_end: if path_like_str == test_str { +// None +// } else { +// Some(path_like_str.len()) +// }, +// }) +// }) +// .unwrap() +// } + +// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { +// FoundPath { +// project: project_path, +// absolute: None, +// } +// } +// } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 97f4262623..f4b8d15d75 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -146,6 +146,11 @@ impl Picker { } } + pub fn refresh(&mut self, cx: &mut ViewContext) { + let query = self.editor.read(cx).text(cx); + self.update_matches(query, cx); + } + pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { let update = self.delegate.update_matches(query, cx); self.matches_updated(cx); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 4ee136f47a..b2b78a2391 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1961,50 +1961,50 @@ impl Workspace { }) } - // pub fn open_abs_path( - // &mut self, - // abs_path: PathBuf, - // visible: bool, - // cx: &mut ViewContext, - // ) -> Task>> { - // cx.spawn(|workspace, mut cx| async move { - // let open_paths_task_result = workspace - // .update(&mut cx, |workspace, cx| { - // workspace.open_paths(vec![abs_path.clone()], visible, cx) - // }) - // .with_context(|| format!("open abs path {abs_path:?} task spawn"))? - // .await; - // anyhow::ensure!( - // open_paths_task_result.len() == 1, - // "open abs path {abs_path:?} task returned incorrect number of results" - // ); - // match open_paths_task_result - // .into_iter() - // .next() - // .expect("ensured single task result") - // { - // Some(open_result) => { - // open_result.with_context(|| format!("open abs path {abs_path:?} task join")) - // } - // None => anyhow::bail!("open abs path {abs_path:?} task returned None"), - // } - // }) - // } + pub fn open_abs_path( + &mut self, + abs_path: PathBuf, + visible: bool, + cx: &mut ViewContext, + ) -> Task>> { + cx.spawn(|workspace, mut cx| async move { + let open_paths_task_result = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_paths(vec![abs_path.clone()], visible, cx) + }) + .with_context(|| format!("open abs path {abs_path:?} task spawn"))? + .await; + anyhow::ensure!( + open_paths_task_result.len() == 1, + "open abs path {abs_path:?} task returned incorrect number of results" + ); + match open_paths_task_result + .into_iter() + .next() + .expect("ensured single task result") + { + Some(open_result) => { + open_result.with_context(|| format!("open abs path {abs_path:?} task join")) + } + None => anyhow::bail!("open abs path {abs_path:?} task returned None"), + } + }) + } - // pub fn split_abs_path( - // &mut self, - // abs_path: PathBuf, - // visible: bool, - // cx: &mut ViewContext, - // ) -> Task>> { - // let project_path_task = - // Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); - // cx.spawn(|this, mut cx| async move { - // let (_, path) = project_path_task.await?; - // this.update(&mut cx, |this, cx| this.split_path(path, cx))? - // .await - // }) - // } + pub fn split_abs_path( + &mut self, + abs_path: PathBuf, + visible: bool, + cx: &mut ViewContext, + ) -> Task>> { + let project_path_task = + Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); + cx.spawn(|this, mut cx| async move { + let (_, path) = project_path_task.await?; + this.update(&mut cx, |this, cx| this.split_path(path, cx))? + .await + }) + } pub fn open_path( &mut self, @@ -2031,37 +2031,37 @@ impl Workspace { }) } - // pub fn split_path( - // &mut self, - // path: impl Into, - // cx: &mut ViewContext, - // ) -> Task, anyhow::Error>> { - // let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { - // self.panes - // .first() - // .expect("There must be an active pane") - // .downgrade() - // }); + pub fn split_path( + &mut self, + path: impl Into, + cx: &mut ViewContext, + ) -> Task, anyhow::Error>> { + let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { + self.panes + .first() + .expect("There must be an active pane") + .downgrade() + }); - // if let Member::Pane(center_pane) = &self.center.root { - // if center_pane.read(cx).items_len() == 0 { - // return self.open_path(path, Some(pane), true, cx); - // } - // } + if let Member::Pane(center_pane) = &self.center.root { + if center_pane.read(cx).items_len() == 0 { + return self.open_path(path, Some(pane), true, cx); + } + } - // let task = self.load_path(path.into(), cx); - // cx.spawn(|this, mut cx| async move { - // let (project_entry_id, build_item) = task.await?; - // this.update(&mut cx, move |this, cx| -> Option<_> { - // let pane = pane.upgrade(cx)?; - // let new_pane = this.split_pane(pane, SplitDirection::Right, cx); - // new_pane.update(cx, |new_pane, cx| { - // Some(new_pane.open_item(project_entry_id, true, cx, build_item)) - // }) - // }) - // .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? - // }) - // } + let task = self.load_path(path.into(), cx); + cx.spawn(|this, mut cx| async move { + let (project_entry_id, build_item) = task.await?; + this.update(&mut cx, move |this, cx| -> Option<_> { + let pane = pane.upgrade()?; + let new_pane = this.split_pane(pane, SplitDirection::Right, cx); + new_pane.update(cx, |new_pane, cx| { + Some(new_pane.open_item(project_entry_id, true, cx, build_item)) + }) + }) + .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? + }) + } pub(crate) fn load_path( &mut self,