From 7d94d8940c69a016ff8cb19ed7665647864aa8ee Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 09:28:18 -0700 Subject: [PATCH 1/6] Not working yet file-finder2 --- Cargo.lock | 25 + crates/file_finder2/Cargo.toml | 36 + crates/file_finder2/src/file_finder.rs | 2172 ++++++++++++++++++++++++ crates/workspace2/src/modal_layer.rs | 8 + crates/workspace2/src/workspace2.rs | 4 + crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- 7 files changed, 2247 insertions(+), 2 deletions(-) create mode 100644 crates/file_finder2/Cargo.toml create mode 100644 crates/file_finder2/src/file_finder.rs diff --git a/Cargo.lock b/Cargo.lock index 0882435df934b8225238ad1ed19d409a723b4d0d..bbc88e67857ae85bf0006db824d3a6e0e51f1cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3061,6 +3061,30 @@ dependencies = [ "workspace", ] +[[package]] +name = "file_finder2" +version = "0.1.0" +dependencies = [ + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fuzzy2", + "gpui2", + "language2", + "menu2", + "picker2", + "postage", + "project2", + "serde", + "serde_json", + "settings2", + "text2", + "theme2", + "util", + "workspace2", +] + [[package]] name = "filetime" version = "0.2.22" @@ -11393,6 +11417,7 @@ dependencies = [ "editor2", "env_logger 0.9.3", "feature_flags2", + "file_finder2", "fs2", "fsevent", "futures 0.3.28", diff --git a/crates/file_finder2/Cargo.toml b/crates/file_finder2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..8950cff792ce66d6a3d2ac7f1671503e7dbd376e --- /dev/null +++ b/crates/file_finder2/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "file_finder2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/file_finder.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +collections = { path = "../collections" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +menu = { package = "menu2", path = "../menu2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +postage.workspace = true +serde.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +theme = { package = "theme2", path = "../theme2", features = ["test-support"] } + +serde_json.workspace = true +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs new file mode 100644 index 0000000000000000000000000000000000000000..a9b5be1dcd7c6d0e9df7231b93b6cee909d78732 --- /dev/null +++ b/crates/file_finder2/src/file_finder.rs @@ -0,0 +1,2172 @@ +use collections::HashMap; +use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; +use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; +use gpui::{actions, AppContext, Task, ViewContext, View, EventEmitter, WindowContext}; +use picker::{Picker, PickerDelegate}; +use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use std::{ + path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; +use text::Point; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; +use workspace::{Workspace, Modal, ModalEvent}; + +actions!(Toggle); + +pub struct FileFinder { + picker: View> +} + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(FileFinder::register); +} + +impl FileFinder { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + workspace.toggle_modal(cx, |cx| FileFinder::new(cx)); + }); + } + + fn new(cx: &mut ViewContext) -> Self { + FileFinder{ + + } + } +} + +impl EventEmitter for FileFinder; +impl Modal for FileFinder{ + fn focus(&self, cx: &mut WindowContext) { + self.picker.update(cx, |picker, cx| { picker.focus(cx) }) + } +} + +pub struct FileFinderDelegate { + workspace: WeakViewHandle, + project: ModelHandle, + search_count: usize, + latest_search_id: usize, + latest_search_did_cancel: bool, + latest_search_query: Option>, + currently_opened_path: Option, + matches: Matches, + selected_index: Option, + cancel_flag: Arc, + history_items: Vec, +} + +#[derive(Debug, Default)] +struct Matches { + history: Vec<(FoundPath, Option)>, + search: Vec, +} + +#[derive(Debug)] +enum Match<'a> { + History(&'a FoundPath, Option<&'a PathMatch>), + Search(&'a PathMatch), +} + +impl Matches { + fn len(&self) -> usize { + self.history.len() + self.search.len() + } + + fn get(&self, index: usize) -> Option> { + if index < self.history.len() { + self.history + .get(index) + .map(|(path, path_match)| Match::History(path, path_match.as_ref())) + } else { + self.search + .get(index - self.history.len()) + .map(Match::Search) + } + } + + fn push_new_matches( + &mut self, + history_items: &Vec, + query: &PathLikeWithPosition, + mut new_search_matches: Vec, + extend_old_matches: bool, + ) { + let matching_history_paths = matching_history_item_paths(history_items, query); + new_search_matches + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + let history_items_to_show = history_items + .iter() + .filter_map(|history_item| { + Some(( + history_item.clone(), + Some( + matching_history_paths + .get(&history_item.project.path)? + .clone(), + ), + )) + }) + .collect::>(); + self.history = history_items_to_show; + if extend_old_matches { + self.search + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + util::extend_sorted( + &mut self.search, + new_search_matches.into_iter(), + 100, + |a, b| b.cmp(a), + ) + } else { + self.search = new_search_matches; + } + } +} + +fn matching_history_item_paths( + history_items: &Vec, + query: &PathLikeWithPosition, +) -> HashMap, PathMatch> { + let history_items_by_worktrees = history_items + .iter() + .filter_map(|found_path| { + let candidate = PathMatchCandidate { + path: &found_path.project.path, + // Only match history items names, otherwise their paths may match too many queries, producing false positives. + // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item, + // it would be shown first always, despite the latter being a better match. + char_bag: CharBag::from_iter( + found_path + .project + .path + .file_name()? + .to_string_lossy() + .to_lowercase() + .chars(), + ), + }; + Some((found_path.project.worktree_id, candidate)) + }) + .fold( + HashMap::default(), + |mut candidates, (worktree_id, new_candidate)| { + candidates + .entry(worktree_id) + .or_insert_with(Vec::new) + .push(new_candidate); + candidates + }, + ); + let mut matching_history_paths = HashMap::default(); + for (worktree, candidates) in history_items_by_worktrees { + let max_results = candidates.len() + 1; + matching_history_paths.extend( + fuzzy::match_fixed_path_set( + candidates, + worktree.to_usize(), + query.path_like.path_query(), + false, + max_results, + ) + .into_iter() + .map(|path_match| (Arc::clone(&path_match.path), path_match)), + ); + } + matching_history_paths +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct FoundPath { + project: ProjectPath, + absolute: Option, +} + +impl FoundPath { + fn new(project: ProjectPath, absolute: Option) -> Self { + Self { project, absolute } + } +} + +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() +} + +#[cfg(test)] +fn history_file_exists(abs_path: &PathBuf) -> bool { + !abs_path.ends_with("nonexistent.rs") +} + +pub enum Event { + Selected(ProjectPath), + Dismissed, +} + +#[derive(Debug, Clone)] +struct FileSearchQuery { + raw_query: String, + file_query_end: Option, +} + +impl FileSearchQuery { + fn path_query(&self) -> &str { + match self.file_query_end { + Some(file_path_end) => &self.raw_query[..file_path_end], + None => &self.raw_query, + } + } +} + +impl FileFinderDelegate { + fn new( + workspace: WeakViewHandle, + project: ModelHandle, + currently_opened_path: Option, + history_items: Vec, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&project, |picker, _, cx| { + picker.update_matches(picker.query(cx), cx); + }) + .detach(); + Self { + workspace, + project, + search_count: 0, + latest_search_id: 0, + latest_search_did_cancel: false, + latest_search_query: None, + currently_opened_path, + matches: Matches::default(), + selected_index: None, + cancel_flag: Arc::new(AtomicBool::new(false)), + history_items, + } + } + + fn spawn_search( + &mut self, + query: PathLikeWithPosition, + cx: &mut ViewContext, + ) -> Task<()> { + let relative_to = self + .currently_opened_path + .as_ref() + .map(|found_path| Arc::clone(&found_path.project.path)); + let worktrees = self + .project + .read(cx) + .visible_worktrees(cx) + .collect::>(); + let include_root_name = worktrees.len() > 1; + let candidate_sets = worktrees + .into_iter() + .map(|worktree| { + let worktree = worktree.read(cx); + PathMatchCandidateSet { + snapshot: worktree.snapshot(), + include_ignored: worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored), + include_root_name, + } + }) + .collect::>(); + + let search_id = util::post_inc(&mut self.search_count); + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + let cancel_flag = self.cancel_flag.clone(); + cx.spawn(|picker, mut cx| async move { + let matches = fuzzy::match_path_sets( + candidate_sets.as_slice(), + query.path_like.path_query(), + relative_to, + false, + 100, + &cancel_flag, + cx.background(), + ) + .await; + let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); + picker + .update(&mut cx, |picker, cx| { + picker + .delegate_mut() + .set_search_matches(search_id, did_cancel, query, matches, cx) + }) + .log_err(); + }) + } + + fn set_search_matches( + &mut self, + search_id: usize, + did_cancel: bool, + query: PathLikeWithPosition, + matches: Vec, + cx: &mut ViewContext, + ) { + if search_id >= self.latest_search_id { + self.latest_search_id = search_id; + let extend_old_matches = self.latest_search_did_cancel + && Some(query.path_like.path_query()) + == self + .latest_search_query + .as_ref() + .map(|query| query.path_like.path_query()); + self.matches + .push_new_matches(&self.history_items, &query, matches, extend_old_matches); + self.latest_search_query = Some(query); + self.latest_search_did_cancel = did_cancel; + cx.notify(); + } + } + + fn labels_for_match( + &self, + path_match: Match, + cx: &AppContext, + ix: usize, + ) -> (String, Vec, String, Vec) { + let (file_name, file_name_positions, full_path, full_path_positions) = match path_match { + Match::History(found_path, found_path_match) => { + let worktree_id = found_path.project.worktree_id; + let project_relative_path = &found_path.project.path; + let has_worktree = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some(); + + if !has_worktree { + if let Some(absolute_path) = &found_path.absolute { + return ( + absolute_path + .file_name() + .map_or_else( + || project_relative_path.to_string_lossy(), + |file_name| file_name.to_string_lossy(), + ) + .to_string(), + Vec::new(), + absolute_path.to_string_lossy().to_string(), + Vec::new(), + ); + } + } + + let mut path = Arc::clone(project_relative_path); + if project_relative_path.as_ref() == Path::new("") { + if let Some(absolute_path) = &found_path.absolute { + path = Arc::from(absolute_path.as_path()); + } + } + + let mut path_match = PathMatch { + score: ix as f64, + positions: Vec::new(), + worktree_id: worktree_id.to_usize(), + path, + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }; + if let Some(found_path_match) = found_path_match { + path_match + .positions + .extend(found_path_match.positions.iter()) + } + + self.labels_for_path_match(&path_match) + } + Match::Search(path_match) => self.labels_for_path_match(path_match), + }; + + if file_name_positions.is_empty() { + if let Some(user_home_path) = std::env::var("HOME").ok() { + let user_home_path = user_home_path.trim(); + if !user_home_path.is_empty() { + if (&full_path).starts_with(user_home_path) { + return ( + file_name, + file_name_positions, + full_path.replace(user_home_path, "~"), + full_path_positions, + ); + } + } + } + } + + ( + file_name, + file_name_positions, + full_path, + full_path_positions, + ) + } + + fn labels_for_path_match( + &self, + path_match: &PathMatch, + ) -> (String, Vec, String, Vec) { + let path = &path_match.path; + let path_string = path.to_string_lossy(); + let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join(""); + let path_positions = path_match.positions.clone(); + + let file_name = path.file_name().map_or_else( + || path_match.path_prefix.to_string(), + |file_name| file_name.to_string_lossy().to_string(), + ); + let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count() + - file_name.chars().count(); + let file_name_positions = path_positions + .iter() + .filter_map(|pos| { + if pos >= &file_name_start { + Some(pos - file_name_start) + } else { + None + } + }) + .collect(); + + (file_name, file_name_positions, full_path, path_positions) + } +} + +impl PickerDelegate for FileFinderDelegate { + fn placeholder_text(&self) -> Arc { + "Search project files...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index.unwrap_or(0) + } + + 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<()> { + if raw_query.is_empty() { + let project = self.project.read(cx); + self.latest_search_id = post_inc(&mut self.search_count); + self.matches = Matches { + history: self + .history_items + .iter() + .filter(|history_item| { + project + .worktree_for_id(history_item.project.worktree_id, cx) + .is_some() + || (project.is_local() && history_item.absolute.is_some()) + }) + .cloned() + .map(|p| (p, None)) + .collect(), + search: Vec::new(), + }; + cx.notify(); + Task::ready(()) + } else { + let raw_query = &raw_query; + let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: raw_query.to_owned(), + file_query_end: if path_like_str == raw_query { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .expect("infallible"); + self.spawn_search(query, cx) + } + } + + 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) { + let open_task = workspace.update(cx, move |workspace, cx| { + let split_or_open = |workspace: &mut Workspace, project_path, cx| { + if secondary { + workspace.split_path(project_path, cx) + } else { + workspace.open_path(project_path, None, true, cx) + } + }; + match m { + Match::History(history_match, _) => { + let worktree_id = history_match.project.worktree_id; + if workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some() + { + split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&history_match.project.path), + }, + cx, + ) + } else { + match history_match.absolute.as_ref() { + Some(abs_path) => { + if secondary { + workspace.split_abs_path( + abs_path.to_path_buf(), + false, + cx, + ) + } else { + workspace.open_abs_path( + abs_path.to_path_buf(), + false, + cx, + ) + } + } + None => split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&history_match.project.path), + }, + cx, + ), + } + } + } + Match::Search(m) => split_or_open( + workspace, + ProjectPath { + worktree_id: WorktreeId::from_usize(m.worktree_id), + path: m.path.clone(), + }, + cx, + ), + } + }); + + let row = self + .latest_search_query + .as_ref() + .and_then(|query| query.row) + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.column) + .unwrap_or(0) + .saturating_sub(1); + cx.spawn(|_, mut cx| async move { + let item = open_task.await.log_err()?; + if let Some(row) = row { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(Point::new(row, col), Bias::Left); + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }) + .log_err(); + } + } + workspace + .downgrade() + .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) + .log_err(); + + Some(()) + }) + .detach(); + } + } + } + + fn dismissed(&mut self, _: &mut ViewContext) {} + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &AppContext, + ) -> AnyElement> { + 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 (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), + ) + .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, + } + } +} diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index b3a5de8fb26d9caf25d652347b9a24ce47818630..bda93a32b9e06165574bb211017946c247f3a26f 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -71,6 +71,14 @@ impl ModalLayer { cx.notify(); } + + pub fn current_modal(&self) -> Option> + where + V: 'static, + { + let active_modal = self.active_modal.as_ref()?; + active_modal.modal.clone().downcast::().ok() + } } impl Render for ModalLayer { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 575ab6b8bd3363901b1f321f068421291c985ae8..4ee136f47af0c3c3c3b4a9d7ca51d79b7b4317d1 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3541,6 +3541,10 @@ impl Workspace { div } + pub fn current_modal(&mut self, cx: &ViewContext) -> Option> { + self.modal_layer.read(cx).current_modal() + } + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 570912abc579a7aad2461c4fe081354c97fcc478..1b4d5b7196a68dccda224afc5e2b8bd07292483f 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -36,7 +36,7 @@ copilot = { package = "copilot2", path = "../copilot2" } db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } # feedback = { path = "../feedback" } -# file_finder = { path = "../file_finder" } +file_finder = { package="file_finder2", path = "../file_finder2" } # search = { path = "../search" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2deaff21491a73bd54bcf5d80a0d2ca9c7a76a7f..a7b1eb02ec50cc6cbc5fcb4e60335b34652f02aa 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -186,7 +186,7 @@ fn main() { // recent_projects::init(cx); go_to_line::init(cx); - // file_finder::init(cx); + file_finder::init(cx); // outline::init(cx); // project_symbols::init(cx); // project_panel::init(Assets, cx); From f4ccff7b726eb38b9f41ce652a8bea841ac76014 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 11:03:55 -0700 Subject: [PATCH 2/6] TEMP --- Cargo.lock | 1 + crates/file_finder2/Cargo.toml | 1 + crates/file_finder2/src/file_finder.rs | 91 ++++++++++++++++++++++---- crates/picker2/src/picker2.rs | 11 +++- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbc88e67857ae85bf0006db824d3a6e0e51f1cf7..2e260f1e49cc9c86211115af41c47902104c9f1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3081,6 +3081,7 @@ dependencies = [ "settings2", "text2", "theme2", + "ui2", "util", "workspace2", ] diff --git a/crates/file_finder2/Cargo.toml b/crates/file_finder2/Cargo.toml index 8950cff792ce66d6a3d2ac7f1671503e7dbd376e..22b9f2cbc825d7205dfbdfcd8bf34930c393b41c 100644 --- a/crates/file_finder2/Cargo.toml +++ b/crates/file_finder2/Cargo.toml @@ -20,6 +20,7 @@ settings = { package = "settings2", path = "../settings2" } text = { package = "text2", path = "../text2" } util = { path = "../util" } theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } workspace = { package = "workspace2", path = "../workspace2" } postage.workspace = true serde.workspace = true diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index a9b5be1dcd7c6d0e9df7231b93b6cee909d78732..67fb1e400feca38318c0511e768f89e5aea42ed9 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -1,7 +1,9 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; -use gpui::{actions, AppContext, Task, ViewContext, View, EventEmitter, WindowContext}; +use gpui::{ + actions, AppContext, Div, EventEmitter, Render, Task, View, ViewContext, WindowContext, +}; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use std::{ @@ -12,13 +14,13 @@ use std::{ }, }; use text::Point; -use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; -use workspace::{Workspace, Modal, ModalEvent}; +use util::{paths::PathLikeWithPosition, post_inc}; +use workspace::{Modal, ModalEvent, Workspace}; actions!(Toggle); pub struct FileFinder { - picker: View> + picker: View>, } pub fn init(cx: &mut AppContext) { @@ -28,21 +30,88 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &Toggle, cx| { - workspace.toggle_modal(cx, |cx| FileFinder::new(cx)); + let Some(file_finder) = workspace.current_modal::(cx) else { + workspace.toggle_modal(cx, |cx| FileFinder::new(workspace, cx)); + return; + }; + file_finder.update(cx, |file_finder, cx| { + file_finder + .picker + .update(cx, |picker, cx| picker.cycle_selection(cx)) + }) }); } - fn new(cx: &mut ViewContext) -> Self { - FileFinder{ + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> Self { + 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 } } -impl EventEmitter for FileFinder; -impl Modal for FileFinder{ +impl EventEmitter for FileFinder {} +impl Modal for FileFinder { fn focus(&self, cx: &mut WindowContext) { - self.picker.update(cx, |picker, cx| { picker.focus(cx) }) + self.picker.update(cx, |picker, cx| picker.focus(cx)) + } +} +impl Render for FileFinder { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) } } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 0cfe5c8992a49a6c56650f02c73845dfd5e1c337..97f4262623b7206455372050f82c8f48c67b804b 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -58,7 +58,7 @@ impl Picker { self.editor.update(cx, |editor, cx| editor.focus(cx)); } - fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { + pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); @@ -98,6 +98,15 @@ impl Picker { } } + pub fn cycle_selection(&mut self, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + let index = self.delegate.selected_index(); + let new_index = if index + 1 == count { 0 } else { index + 1 }; + self.delegate.set_selected_index(new_index, cx); + self.scroll_handle.scroll_to_item(new_index); + cx.notify(); + } + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { self.delegate.dismissed(cx); } From 3a4c5aa44087af95dc34ec3e536c6fd825a79b14 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 13:34:14 -0700 Subject: [PATCH 3/6] Implement FileFinder --- crates/file_finder2/src/file_finder.rs | 3121 ++++++++++++------------ crates/picker2/src/picker2.rs | 5 + crates/workspace2/src/workspace2.rs | 144 +- 3 files changed, 1615 insertions(+), 1655 deletions(-) diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 67fb1e400feca38318c0511e768f89e5aea42ed9..13296887cb2266bf544475ac6a3ef80a45dcebbc 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), - ) - .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", - } - }), + 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)), ) - .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 97f4262623b7206455372050f82c8f48c67b804b..f4b8d15d75d904863a0b6829cd9ac14b004c75ac 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 4ee136f47af0c3c3c3b4a9d7ca51d79b7b4317d1..b2b78a23915e5436879e72ed8c0db49426ca9a78 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, From 3b01a032ba22ab863773b6e1c9f214b0594d723b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 14:38:23 -0700 Subject: [PATCH 4/6] In the middle of stuff --- crates/editor2/src/element.rs | 1 + crates/file_finder2/src/file_finder.rs | 2693 +++++++++++------------- crates/gpui2/src/app/test_context.rs | 34 +- crates/gpui2/src/window.rs | 3 + crates/workspace2/src/workspace2.rs | 20 +- 5 files changed, 1278 insertions(+), 1473 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 638ed3389191e364d0a382c285b0af456db3ee92..a68825fa77657d5e081019d5ba95b85820f59b7d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1448,6 +1448,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 13296887cb2266bf544475ac6a3ef80a45dcebbc..c460cac252c01d3efa96bf718a4bbe0f996e8e7b 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -32,9 +32,9 @@ pub fn init(cx: &mut AppContext) { impl FileFinder { fn register(workspace: &mut Workspace, _: &mut ViewContext) { - dbg!("yay"); + dbg!("REGISTERING"); workspace.register_action(|workspace, _: &Toggle, cx| { - dbg!("yayer"); + dbg!("CALLING ACTION"); let Some(file_finder) = workspace.current_modal::(cx) else { Self::open(workspace, cx); return; @@ -738,1459 +738,1236 @@ impl PickerDelegate for FileFinderDelegate { } } -// #[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, -// } -// } -// } +#[cfg(test)] +mod tests { + use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + + use super::*; + use editor::Editor; + use gpui::{Entity, TestAppContext, VisualTestContext}; + 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 (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.update_matches("bna".to_string(), cx) + }) + .await; + + picker.update(cx, |picker, _| { + assert_eq!(picker.delegate.matches.len(), 2); + }); + + let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); + cx.dispatch_action(SelectNext); + cx.dispatch_action(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 + .to_any() + .downcast::() + .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 (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + 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}"); + picker + .update(cx, |finder, cx| { + finder + .delegate + .update_matches(query_inside_file.to_string(), cx) + }) + .await; + picker.update(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(SelectNext); + cx.dispatch_action(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.executor().advance_clock(Duration::from_secs(2)); + + 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 (picker, workspace, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + 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}"); + picker + .update(cx, |picker, cx| { + picker + .delegate + .update_matches(query_outside_file.to_string(), cx) + }) + .await; + picker.update(cx, |finder, _| { + let delegate = &finder.delegate; + assert_eq!(delegate.matches.len(), 1); + let latest_search_query = delegate + .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(SelectNext); + cx.dispatch_action(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.executor().advance_clock(Duration::from_secs(2)); + + 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 (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + let query = test_path_like("hi"); + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(query.clone(), cx) + }) + .await; + + picker.update(cx, |picker, _cx| { + assert_eq!(picker.delegate.matches.len(), 5) + }); + + picker.update(cx, |picker, cx| { + let delegate = &mut picker.delegate; + 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 (picker, _, mut cx) = build_find_picker(project, cx); + let cx = &mut cx; + + picker + .update(cx, |picker, cx| { + picker.delegate.spawn_search(test_path_like("hi"), cx) + }) + .await; + picker.update(cx, |picker, _| assert_eq!(picker.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 (picker, _, mut cx) = build_find_picker(project, cx); + // let cx = &mut cx; + + // // Even though there is only one worktree, that worktree's filename + // // is included in the matching, because the worktree is a single file. + // picker + // .update(cx, |picker, cx| { + // picker.delegate.spawn_search(test_path_like("thf"), cx) + // }) + // .await; + // cx.read(|cx| { + // let picker = picker.read(cx); + // let delegate = &picker.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. + // picker + // .update(cx, |f, cx| { + // f.delegate.spawn_search(test_path_like("thf/"), cx) + // }) + // .await; + // picker.update(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) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut 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")), + // })); + // cx.dispatch_action(Toggle); + + // 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.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.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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut 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", &workspace, 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", &workspace, 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", &workspace, 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", &workspace, 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", &workspace, 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(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(); + // cx.background_executor.run_until_parked(); + + // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut 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(); + // cx.background_executor.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.entity_id() != worktree_id.to_usize()) + // .expect("New worktree should have a different id") + // .id(), + // ) + // }); + // close_active_item(&workspace, cx).await; + + // let initial_history_items = + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, 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", &workspace, 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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + let cx = &mut cx; + + // generate some history to select from + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + let current_history = + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + for expected_selected_index in 0..current_history.len() { + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .read(cx) + .delegate + .selected_index() + }); + assert_eq!( + selected_index, expected_selected_index, + "Should select the next item in the history" + ); + } + + cx.dispatch_action(Toggle); + let selected_index = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut 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].entity_id()) + // }); + + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let first_query = "f"; + // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.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 = workspace.update(cx, |workspace, cx| { + // workspace + // .current_modal::(cx) + // .unwrap() + // .read(cx) + // .picker + // }); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.update_matches(second_query.to_string(), cx) + // }) + // .await; + // finder.update(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 = workspace.update(cx, |workspace, cx| { + // workspace + // .current_modal::(cx) + // .unwrap() + // .read(cx) + // .picker + // }); + // finder + // .update(cx, |finder, cx| { + // finder + // .delegate + // .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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let query = "collab_ui"; + // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.delegate.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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + // let cx = &mut cx; + // // generate some history to select from + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + // open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; + // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + + // cx.dispatch_action(Toggle); + // let query = "rs"; + // let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); + // finder + // .update(cx, |finder, cx| { + // finder.picker.update(cx, |picker, cx| { + // picker.delegate.update_matches(query.to_string(), cx) + // }) + // }) + // .await; + // finder.update(cx, |finder, _| { + // let history_entries = finder.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, + workspace: &View, + cx: &mut gpui::VisualTestContext<'_>, + ) -> Vec { + cx.dispatch_action(Toggle); + let picker = workspace.update(cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + picker + .update(cx, |finder, cx| { + finder.delegate.update_matches(input.to_string(), cx) + }) + .await; + let history_items = picker.update(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(SelectNext); + cx.dispatch_action(Confirm); + cx.background_executor.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 + .to_any() + .downcast::() + .unwrap() + .read(cx) + .title(cx); + assert_eq!( + expected_editor_title, active_editor_title, + "Unexpected editor title for query {input}" + ); + }); + + close_active_item(workspace, cx).await; + + history_items + } + + async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { + let mut original_items = HashMap::new(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.entity_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(); + cx.background_executor.run_until_parked(); + cx.read(|cx| { + for pane in workspace.read(cx).panes() { + let pane_id = pane.entity_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.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, + } + } + + fn build_find_picker( + project: Model, + cx: &mut TestAppContext, + ) -> ( + View>, + View, + VisualTestContext, + ) { + let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); + cx.dispatch_action(Toggle); + let picker = workspace.update(&mut cx, |workspace, cx| { + workspace + .current_modal::(cx) + .unwrap() + .read(cx) + .picker + .clone() + }); + (picker, workspace, cx) + } +} diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 44c31bbd69dad1325da8ec062551c3c03d67d7e2..850ddd6c9ad7d8c822781bb37420e906eac6ef20 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ - div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, - Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, - ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, - VisualContext, WindowContext, WindowHandle, WindowOptions, + div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, + BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TextStyle, + View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -83,8 +83,16 @@ impl TestAppContext { )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); + let cx = AppContext::new(platform, asset_source, http_client); + let lock = cx.borrow_mut(); + lock.push_text_style(crate::TextStyleRefinement { + font_family: "Helvetica".into(), + ..Default::default() + }); + drop(lock); + Self { - app: AppContext::new(platform, asset_source, http_client), + app: cx, background_executor, foreground_executor, dispatcher: dispatcher.clone(), @@ -199,6 +207,15 @@ impl TestAppContext { } } + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .unwrap() + } + pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -376,6 +393,13 @@ impl<'a> VisualTestContext<'a> { pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } + + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index efb586fe038efa205bf6a05dad48cbc94f32defc..acbe851b4d631a533b1caa5e527626606247ef9b 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -422,8 +422,11 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { + dbg!("BEFORE FOCUS"); if let Some(focus_handle) = self.focused() { + dbg!("BEFORE DEFER", focus_handle.id); self.defer(move |cx| { + dbg!("AFTER DEFER"); if let Some(node_id) = cx .window .current_frame diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index db012da38baf23c3dcc0c48df16a1f7b5b69c25a..247c738161fa59e87d602e96a8994ac4fcb22e01 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + FocusHandle, FocusableKeyDispatch, GlobalPixels, KeyContext, Model, ModelContext, + ParentElement, Point, Render, Size, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3409,10 +3409,6 @@ impl Workspace { // }); } - // todo!() - // #[cfg(any(test, feature = "test-support"))] - // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { - // use node_runtime::FakeNodeRuntime; #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { use gpui::Context; @@ -3432,7 +3428,10 @@ impl Workspace { initialize_workspace: |_, _, _, _| Task::ready(Ok(())), node_runtime: FakeNodeRuntime::new(), }); - Self::new(0, project, app_state, cx) + let workspace = Self::new(0, project, app_state, cx); + dbg!(&workspace.focus_handle); + workspace.focus_handle.focus(cx); + workspace } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { @@ -3710,13 +3709,14 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); self.add_workspace_actions_listeners(div()) + .track_focus(&self.focus_handle) .context(context) .relative() .size_full() From 6b25841e2a2d22cba6669ccef4652d89dcfffe5e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 14:48:34 -0800 Subject: [PATCH 5/6] WIP --- crates/editor2/src/editor.rs | 4 ++-- crates/editor2/src/element.rs | 1 + crates/file_finder2/src/file_finder.rs | 3 ++- crates/gpui2/src/app/test_context.rs | 13 +++---------- crates/theme2/src/one_themes.rs | 1 + crates/theme2/src/settings.rs | 7 +++++++ crates/ui2/src/components/tooltip.rs | 6 ++++-- crates/ui2/src/to_extract/workspace.rs | 3 ++- crates/workspace2/src/workspace2.rs | 5 +++-- 9 files changed, 25 insertions(+), 18 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ebe78d95b3ea942e3cdcbcfb5a1b2dad5c09ed89..84a80c9ebcbbcf8eff461ac5e4b5ffc08d70bab2 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9379,8 +9379,8 @@ impl Render for Editor { EditorMode::SingleLine => { TextStyle { color: cx.theme().colors().text, - font_family: "Zed Sans".into(), // todo!() - font_features: FontFeatures::default(), + font_family: settings.ui_font.family.clone(), // todo!() + font_features: settings.ui_font.features, font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index a68825fa77657d5e081019d5ba95b85820f59b7d..4f7156a74720db1ab53b98a62942ac8044139b64 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1448,6 +1448,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index c460cac252c01d3efa96bf718a4bbe0f996e8e7b..2b78a24dea6bc7dca05ccf85643478d9526f10e1 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -39,11 +39,12 @@ impl FileFinder { Self::open(workspace, cx); return; }; + file_finder.update(cx, |file_finder, cx| { file_finder .picker .update(cx, |picker, cx| picker.cycle_selection(cx)) - }) + }); }); } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 850ddd6c9ad7d8c822781bb37420e906eac6ef20..50447b29462c88a9ef4387a1b72abbe5e9f5c366 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, - Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TextStyle, - View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, + ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -83,16 +83,9 @@ impl TestAppContext { )); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); - let cx = AppContext::new(platform, asset_source, http_client); - let lock = cx.borrow_mut(); - lock.push_text_style(crate::TextStyleRefinement { - font_family: "Helvetica".into(), - ..Default::default() - }); - drop(lock); Self { - app: cx, + app: AppContext::new(platform, asset_source, http_client), background_executor, foreground_executor, dispatcher: dispatcher.clone(), diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index 6e32eace7350414b002dfda1d3bf242665401e74..733cd6c40b6c789f484890b7a7591e5ae3cc6d40 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -35,6 +35,7 @@ pub(crate) fn one_dark() -> Theme { id: "one_dark".to_string(), name: "One Dark".into(), appearance: Appearance::Dark, + styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors { diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 8a15b52641bc30ca03e4d32c65070b89d5b8e79c..5e3329ffa149b1f744f7031162c7235961c42a96 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -19,6 +19,7 @@ const MIN_LINE_HEIGHT: f32 = 1.0; #[derive(Clone)] pub struct ThemeSettings { pub ui_font_size: Pixels, + pub ui_font: Font, pub buffer_font: Font, pub buffer_font_size: Pixels, pub buffer_line_height: BufferLineHeight, @@ -120,6 +121,12 @@ impl settings::Settings for ThemeSettings { let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(), + ui_font: Font { + family: "Helvetica".into(), + features: Default::default(), + weight: Default::default(), + style: Default::default(), + }, buffer_font: Font { family: defaults.buffer_font_family.clone().unwrap().into(), features: defaults.buffer_font_features.clone().unwrap(), diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 58375b0b6746a3cffbd4534cfd57faef02574efd..8463ed7ba4a34a1ccb110499db8dbe0c61d0f9b8 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,5 +1,6 @@ use gpui::{Div, Render}; -use theme2::ActiveTheme; +use settings2::Settings; +use theme2::{ActiveTheme, ThemeSettings}; use crate::prelude::*; use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor}; @@ -34,9 +35,10 @@ impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); v_stack() .elevation_2(cx) - .font("Zed Sans") + .font(ui_font) .text_ui_sm() .text_color(cx.theme().colors().text) .py_1() diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index d6de8a828807606a6d55086af724983ca1a93df0..0451a9d032666f827335d843c2c06124650300d1 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -206,13 +206,14 @@ impl Render for Workspace { .child(self.editor_1.clone())], SplitDirection::Horizontal, ); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); div() .relative() .size_full() .flex() .flex_col() - .font("Zed Sans") + .font(ui_font) .gap_0() .justify_start() .items_start() diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 247c738161fa59e87d602e96a8994ac4fcb22e01..4786e7e35de96f2f7e7af2c96fface1c7278faa7 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -67,7 +67,7 @@ use std::{ sync::{atomic::AtomicUsize, Arc}, time::Duration, }; -use theme2::ActiveTheme; +use theme2::{ActiveTheme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip}; use util::ResultExt; @@ -3714,6 +3714,7 @@ impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); self.add_workspace_actions_listeners(div()) .track_focus(&self.focus_handle) @@ -3722,7 +3723,7 @@ impl Render for Workspace { .size_full() .flex() .flex_col() - .font("Zed Sans") + .font(ui_font) .gap_0() .justify_start() .items_start() From 1109cd11c81e21de4b9e6762b843a168d822cec5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 14 Nov 2023 16:16:02 -0700 Subject: [PATCH 6/6] Abandon ship --- crates/editor2/src/element.rs | 1 - crates/file_finder2/src/file_finder.rs | 2468 ++++++++++---------- crates/gpui2/src/elements/uniform_list.rs | 2 +- crates/project_panel2/src/project_panel.rs | 3 +- crates/workspace2/src/workspace2.rs | 22 +- 5 files changed, 1247 insertions(+), 1249 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 4f7156a74720db1ab53b98a62942ac8044139b64..4f3bda3752adddf53c1869a9846b08941a9e2211 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1449,7 +1449,6 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); - dbg!(&style.text.font()); let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 2b78a24dea6bc7dca05ccf85643478d9526f10e1..72638f603f8005626ce7baa219669b8de1b88dc6 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -593,6 +593,7 @@ impl PickerDelegate for FileFinderDelegate { } fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + dbg!("CONFIRMING???"); if let Some(m) = self.matches.get(self.selected_index()) { if let Some(workspace) = self.workspace.upgrade() { let open_task = workspace.update(cx, move |workspace, cx| { @@ -690,6 +691,7 @@ impl PickerDelegate for FileFinderDelegate { .log_err(); } } + dbg!("DISMISSING"); finder .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) .ok()?; @@ -739,1236 +741,1236 @@ impl PickerDelegate for FileFinderDelegate { } } -#[cfg(test)] -mod tests { - use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; - - use super::*; - use editor::Editor; - use gpui::{Entity, TestAppContext, VisualTestContext}; - 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 (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - picker - .update(cx, |picker, cx| { - picker.delegate.update_matches("bna".to_string(), cx) - }) - .await; - - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 2); - }); - - let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(SelectNext); - cx.dispatch_action(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 - .to_any() - .downcast::() - .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 (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - 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}"); - picker - .update(cx, |finder, cx| { - finder - .delegate - .update_matches(query_inside_file.to_string(), cx) - }) - .await; - picker.update(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(SelectNext); - cx.dispatch_action(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.executor().advance_clock(Duration::from_secs(2)); - - 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 (picker, workspace, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - 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}"); - picker - .update(cx, |picker, cx| { - picker - .delegate - .update_matches(query_outside_file.to_string(), cx) - }) - .await; - picker.update(cx, |finder, _| { - let delegate = &finder.delegate; - assert_eq!(delegate.matches.len(), 1); - let latest_search_query = delegate - .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(SelectNext); - cx.dispatch_action(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.executor().advance_clock(Duration::from_secs(2)); - - 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 (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - let query = test_path_like("hi"); - picker - .update(cx, |picker, cx| { - picker.delegate.spawn_search(query.clone(), cx) - }) - .await; - - picker.update(cx, |picker, _cx| { - assert_eq!(picker.delegate.matches.len(), 5) - }); - - picker.update(cx, |picker, cx| { - let delegate = &mut picker.delegate; - 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 (picker, _, mut cx) = build_find_picker(project, cx); - let cx = &mut cx; - - picker - .update(cx, |picker, cx| { - picker.delegate.spawn_search(test_path_like("hi"), cx) - }) - .await; - picker.update(cx, |picker, _| assert_eq!(picker.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 (picker, _, mut cx) = build_find_picker(project, cx); - // let cx = &mut cx; - - // // Even though there is only one worktree, that worktree's filename - // // is included in the matching, because the worktree is a single file. - // picker - // .update(cx, |picker, cx| { - // picker.delegate.spawn_search(test_path_like("thf"), cx) - // }) - // .await; - // cx.read(|cx| { - // let picker = picker.read(cx); - // let delegate = &picker.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. - // picker - // .update(cx, |f, cx| { - // f.delegate.spawn_search(test_path_like("thf/"), cx) - // }) - // .await; - // picker.update(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) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut 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")), - // })); - // cx.dispatch_action(Toggle); - - // 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.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.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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut 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", &workspace, 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", &workspace, 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", &workspace, 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", &workspace, 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", &workspace, 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(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(); - // cx.background_executor.run_until_parked(); - - // let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut 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(); - // cx.background_executor.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.entity_id() != worktree_id.to_usize()) - // .expect("New worktree should have a different id") - // .id(), - // ) - // }); - // close_active_item(&workspace, cx).await; - - // let initial_history_items = - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, 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", &workspace, 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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - let cx = &mut cx; - - // generate some history to select from - open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - let current_history = - open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - - for expected_selected_index in 0..current_history.len() { - cx.dispatch_action(Toggle); - let selected_index = workspace.update(cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .read(cx) - .delegate - .selected_index() - }); - assert_eq!( - selected_index, expected_selected_index, - "Should select the next item in the history" - ); - } - - cx.dispatch_action(Toggle); - let selected_index = workspace.update(cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut 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].entity_id()) - // }); - - // // generate some history to select from - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - - // cx.dispatch_action(Toggle); - // let first_query = "f"; - // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - // finder - // .update(cx, |finder, cx| { - // finder.delegate.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 = workspace.update(cx, |workspace, cx| { - // workspace - // .current_modal::(cx) - // .unwrap() - // .read(cx) - // .picker - // }); - // finder - // .update(cx, |finder, cx| { - // finder.delegate.update_matches(second_query.to_string(), cx) - // }) - // .await; - // finder.update(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 = workspace.update(cx, |workspace, cx| { - // workspace - // .current_modal::(cx) - // .unwrap() - // .read(cx) - // .picker - // }); - // finder - // .update(cx, |finder, cx| { - // finder - // .delegate - // .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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // // generate some history to select from - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; - - // cx.dispatch_action(Toggle); - // let query = "collab_ui"; - // let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); - // finder - // .update(cx, |finder, cx| { - // finder.delegate.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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - // let cx = &mut cx; - // // generate some history to select from - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - // open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; - // open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; - // open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; - - // cx.dispatch_action(Toggle); - // let query = "rs"; - // let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); - // finder - // .update(cx, |finder, cx| { - // finder.picker.update(cx, |picker, cx| { - // picker.delegate.update_matches(query.to_string(), cx) - // }) - // }) - // .await; - // finder.update(cx, |finder, _| { - // let history_entries = finder.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, - workspace: &View, - cx: &mut gpui::VisualTestContext<'_>, - ) -> Vec { - cx.dispatch_action(Toggle); - let picker = workspace.update(cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - picker - .update(cx, |finder, cx| { - finder.delegate.update_matches(input.to_string(), cx) - }) - .await; - let history_items = picker.update(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(SelectNext); - cx.dispatch_action(Confirm); - cx.background_executor.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 - .to_any() - .downcast::() - .unwrap() - .read(cx) - .title(cx); - assert_eq!( - expected_editor_title, active_editor_title, - "Unexpected editor title for query {input}" - ); - }); - - close_active_item(workspace, cx).await; - - history_items - } - - async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { - let mut original_items = HashMap::new(); - cx.read(|cx| { - for pane in workspace.read(cx).panes() { - let pane_id = pane.entity_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(); - cx.background_executor.run_until_parked(); - cx.read(|cx| { - for pane in workspace.read(cx).panes() { - let pane_id = pane.entity_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.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, - } - } - - fn build_find_picker( - project: Model, - cx: &mut TestAppContext, - ) -> ( - View>, - View, - VisualTestContext, - ) { - let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(Toggle); - let picker = workspace.update(&mut cx, |workspace, cx| { - workspace - .current_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - (picker, workspace, cx) - } -} +// #[cfg(test)] +// mod tests { +// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + +// use super::*; +// use editor::Editor; +// use gpui::{Entity, TestAppContext, VisualTestContext}; +// 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 (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// picker +// .update(cx, |picker, cx| { +// picker.delegate.update_matches("bna".to_string(), cx) +// }) +// .await; + +// picker.update(cx, |picker, _| { +// assert_eq!(picker.delegate.matches.len(), 2); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(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 +// .to_any() +// .downcast::() +// .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 (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// 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}"); +// picker +// .update(cx, |finder, cx| { +// finder +// .delegate +// .update_matches(query_inside_file.to_string(), cx) +// }) +// .await; +// picker.update(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(SelectNext); +// cx.dispatch_action(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.executor().advance_clock(Duration::from_secs(2)); + +// 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 (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// 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}"); +// picker +// .update(cx, |picker, cx| { +// picker +// .delegate +// .update_matches(query_outside_file.to_string(), cx) +// }) +// .await; +// picker.update(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.len(), 1); +// let latest_search_query = delegate +// .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(SelectNext); +// cx.dispatch_action(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.executor().advance_clock(Duration::from_secs(2)); + +// 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 (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let query = test_path_like("hi"); +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(query.clone(), cx) +// }) +// .await; + +// picker.update(cx, |picker, _cx| { +// assert_eq!(picker.delegate.matches.len(), 5) +// }); + +// picker.update(cx, |picker, cx| { +// let delegate = &mut picker.delegate; +// 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 (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(test_path_like("hi"), cx) +// }) +// .await; +// picker.update(cx, |picker, _| assert_eq!(picker.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 (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// // Even though there is only one worktree, that worktree's filename +// // is included in the matching, because the worktree is a single file. +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(test_path_like("thf"), cx) +// }) +// .await; +// cx.read(|cx| { +// let picker = picker.read(cx); +// let delegate = &picker.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. +// picker +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("thf/"), cx) +// }) +// .await; +// picker.update(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) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut 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")), +// })); +// cx.dispatch_action(Toggle); + +// 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.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.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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut 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", &workspace, 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", &workspace, 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", &workspace, 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", &workspace, 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", &workspace, 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(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(); +// cx.background_executor.run_until_parked(); + +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut 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(); +// cx.background_executor.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.entity_id() != worktree_id.to_usize()) +// .expect("New worktree should have a different id") +// .id(), +// ) +// }); +// close_active_item(&workspace, cx).await; + +// let initial_history_items = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, 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", &workspace, 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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; + +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// cx.executor().run_until_parked(); +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// for expected_selected_index in 0..current_history.len() { +// cx.dispatch_action(Toggle); +// let selected_index = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .read(cx) +// .delegate +// .selected_index() +// }); +// assert_eq!( +// selected_index, expected_selected_index, +// "Should select the next item in the history" +// ); +// } + +// cx.dispatch_action(Toggle); +// let selected_index = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut 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].entity_id()) +// }); + +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let first_query = "f"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.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 = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// }); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(second_query.to_string(), cx) +// }) +// .await; +// finder.update(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 = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// }); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate +// .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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let query = "collab_ui"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.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(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 (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let query = "rs"; +// let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.picker.update(cx, |picker, cx| { +// picker.delegate.update_matches(query.to_string(), cx) +// }) +// }) +// .await; +// finder.update(cx, |finder, _| { +// let history_entries = finder.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, +// workspace: &View, +// cx: &mut gpui::VisualTestContext<'_>, +// ) -> Vec { +// cx.dispatch_action(Toggle); +// let picker = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .clone() +// }); +// picker +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(input.to_string(), cx) +// }) +// .await; +// let history_items = picker.update(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(SelectNext); +// cx.dispatch_action(Confirm); +// cx.background_executor.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 +// .to_any() +// .downcast::() +// .unwrap() +// .read(cx) +// .title(cx); +// assert_eq!( +// expected_editor_title, active_editor_title, +// "Unexpected editor title for query {input}" +// ); +// }); + +// close_active_item(workspace, cx).await; + +// history_items +// } + +// async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { +// let mut original_items = HashMap::new(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.entity_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(); +// cx.background_executor.run_until_parked(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.entity_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.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, +// } +// } + +// fn build_find_picker( +// project: Model, +// cx: &mut TestAppContext, +// ) -> ( +// View>, +// View, +// VisualTestContext, +// ) { +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// cx.dispatch_action(Toggle); +// let picker = workspace.update(&mut cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .clone() +// }); +// (picker, workspace, cx) +// } +// } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 6687559d1c811349a3f18a5585f2d2d4110eb16e..c81ff5f26a55111af5bef6db02135eb251719dc9 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -15,7 +15,7 @@ use taffy::style::Overflow; pub fn uniform_list( id: Id, item_count: usize, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> SmallVec<[C; 64]>, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, ) -> UniformList where Id: Into, diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 1feead1a19e1b08d9ae5b0cca15095f802e997f5..b39d62c9a1527629306316c26e926fb30c99e111 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -21,7 +21,6 @@ use project::{ }; use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use std::{ cmp::Ordering, collections::{hash_map, HashMap}, @@ -1468,7 +1467,7 @@ impl Render for ProjectPanel { .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), |this: &mut Self, range, cx| { - let mut items = SmallVec::new(); + let mut items = Vec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { items.push(this.render_entry(id, details, cx)); }); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 4786e7e35de96f2f7e7af2c96fface1c7278faa7..0101b60f8805e3ffeadf7404fad764568b07d1b3 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableKeyDispatch, GlobalPixels, KeyContext, Model, ModelContext, - ParentElement, Point, Render, Size, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, + FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, + StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3037,10 +3037,10 @@ impl Workspace { fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { self.panes.retain(|p| p != pane); - if true { - todo!() - // cx.focus(self.panes.last().unwrap()); - } + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| pane.focus(cx)); if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; } @@ -3429,8 +3429,7 @@ impl Workspace { node_runtime: FakeNodeRuntime::new(), }); let workspace = Self::new(0, project, app_state, cx); - dbg!(&workspace.focus_handle); - workspace.focus_handle.focus(cx); + workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); workspace } @@ -3709,7 +3708,7 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA impl EventEmitter for Workspace {} impl Render for Workspace { - type Element = Div, FocusableKeyDispatch>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); @@ -3717,7 +3716,6 @@ impl Render for Workspace { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); self.add_workspace_actions_listeners(div()) - .track_focus(&self.focus_handle) .context(context) .relative() .size_full()