@@ -0,0 +1,1977 @@
+use collections::HashMap;
+use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
+use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
+use gpui::{
+ actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
+ ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
+ 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 theme::ActiveTheme;
+use ui::{v_stack, HighlightedLabel, StyledExt};
+use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
+use workspace::{Modal, ModalEvent, Workspace};
+
+actions!(Toggle);
+
+pub struct FileFinder {
+ picker: View<Picker<FileFinderDelegate>>,
+}
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(FileFinder::register).detach();
+}
+
+impl FileFinder {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ dbg!("REGISTERING");
+ workspace.register_action(|workspace, _: &Toggle, cx| {
+ dbg!("CALLING ACTION");
+ let Some(file_finder) = workspace.current_modal::<Self>(cx) else {
+ Self::open(workspace, cx);
+ return;
+ };
+
+ file_finder.update(cx, |file_finder, cx| {
+ file_finder
+ .picker
+ .update(cx, |picker, cx| picker.cycle_selection(cx))
+ });
+ });
+ }
+
+ fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+ 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 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)
+ });
+ }
+
+ fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
+ Self {
+ picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+ }
+ }
+}
+
+impl EventEmitter<ModalEvent> for FileFinder {}
+impl Modal for FileFinder {
+ fn focus(&self, cx: &mut WindowContext) {
+ self.picker.update(cx, |picker, cx| picker.focus(cx))
+ }
+}
+impl Render for FileFinder {
+ type Element = Div<Self>;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().w_96().child(self.picker.clone())
+ }
+}
+
+pub struct FileFinderDelegate {
+ file_finder: WeakView<FileFinder>,
+ workspace: WeakView<Workspace>,
+ project: Model<Project>,
+ search_count: usize,
+ latest_search_id: usize,
+ latest_search_did_cancel: bool,
+ latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
+ currently_opened_path: Option<FoundPath>,
+ matches: Matches,
+ selected_index: Option<usize>,
+ cancel_flag: Arc<AtomicBool>,
+ history_items: Vec<FoundPath>,
+}
+
+#[derive(Debug, Default)]
+struct Matches {
+ history: Vec<(FoundPath, Option<PathMatch>)>,
+ search: Vec<PathMatch>,
+}
+
+#[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<Match<'_>> {
+ 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<FoundPath>,
+ query: &PathLikeWithPosition<FileSearchQuery>,
+ mut new_search_matches: Vec<PathMatch>,
+ 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::<Vec<_>>();
+ 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<FoundPath>,
+ query: &PathLikeWithPosition<FileSearchQuery>,
+) -> HashMap<Arc<Path>, 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<PathBuf>,
+}
+
+impl FoundPath {
+ fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
+ Self { project, absolute }
+ }
+}
+
+const MAX_RECENT_SELECTIONS: usize = 20;
+
+#[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<usize>,
+}
+
+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(
+ file_finder: WeakView<FileFinder>,
+ workspace: WeakView<Workspace>,
+ project: Model<Project>,
+ currently_opened_path: Option<FoundPath>,
+ history_items: Vec<FoundPath>,
+ cx: &mut ViewContext<FileFinder>,
+ ) -> Self {
+ 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,
+ 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<FileSearchQuery>,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> 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::<Vec<_>>();
+ 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::<Vec<_>>();
+
+ 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_executor().clone(),
+ )
+ .await;
+ let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+ picker
+ .update(&mut cx, |picker, cx| {
+ picker
+ .delegate
+ .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<FileSearchQuery>,
+ matches: Vec<PathMatch>,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) {
+ 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<usize>, String, Vec<usize>) {
+ 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<usize>, String, Vec<usize>) {
+ 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 {
+ type ListItem = Div<Picker<Self>>;
+
+ fn placeholder_text(&self) -> Arc<str> {
+ "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<Picker<Self>>) {
+ self.selected_index = Some(ix);
+ cx.notify();
+ }
+
+ fn update_matches(
+ &mut self,
+ raw_query: String,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> 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<Picker<FileFinderDelegate>>) {
+ 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| {
+ 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);
+ let finder = self.file_finder.clone();
+
+ 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::<Editor>() {
+ 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();
+ }
+ }
+ dbg!("DISMISSING");
+ finder
+ .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+ .ok()?;
+
+ Some(())
+ })
+ .detach();
+ }
+ }
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
+ self.file_finder
+ .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+ .log_err();
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Self::ListItem {
+ let path_match = self
+ .matches
+ .get(ix)
+ .expect("Invalid matches state: no element for index {ix}");
+ 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);
+
+ 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)),
+ )
+ }
+}
+
+// #[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::<Editor>()
+// .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::<Editor>().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::<Editor>().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::<Vec<_>>();
+// 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::<Vec<_>>();
+// 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::<Vec<_>>();
+// 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::<Vec<_>>();
+// 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::<FileFinder>(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::<FileFinder>(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::<Vec<_>>();
+// 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::<FileFinder>().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::<FileFinder>(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::<FileFinder>(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::<FileFinder>().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::<Vec<_>>();
+// 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::<FileFinder>().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::<Vec<_>>();
+// 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<Workspace>,
+// cx: &mut gpui::VisualTestContext<'_>,
+// ) -> Vec<FoundPath> {
+// cx.dispatch_action(Toggle);
+// let picker = workspace.update(cx, |workspace, cx| {
+// workspace
+// .current_modal::<FileFinder>(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::<Editor>()
+// .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<Workspace>, 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<AppState> {
+// 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<FileSearchQuery> {
+// 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<Project>,
+// cx: &mut TestAppContext,
+// ) -> (
+// View<Picker<FileFinderDelegate>>,
+// View<Workspace>,
+// 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::<FileFinder>(cx)
+// .unwrap()
+// .read(cx)
+// .picker
+// .clone()
+// });
+// (picker, workspace, cx)
+// }
+// }
@@ -8,7 +8,7 @@ use file_associations::FileAssociations;
use anyhow::{anyhow, Result};
use gpui::{
- actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
@@ -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},
@@ -31,7 +30,7 @@ use std::{
sync::Arc,
};
use theme::ActiveTheme as _;
-use ui::{h_stack, v_stack, Label};
+use ui::{h_stack, v_stack, IconElement, Label};
use unicase::UniCase;
use util::{maybe, TryFutureExt};
use workspace::{
@@ -197,23 +196,20 @@ impl ProjectPanel {
editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
this.autoscroll(cx);
}
+ editor::Event::Blurred => {
+ if this
+ .edit_state
+ .as_ref()
+ .map_or(false, |state| state.processing_filename.is_none())
+ {
+ this.edit_state = None;
+ this.update_visible_entries(None, cx);
+ }
+ }
_ => {}
})
.detach();
- // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
- // if !is_focused
- // && this
- // .edit_state
- // .as_ref()
- // .map_or(false, |state| state.processing_filename.is_none())
- // {
- // this.edit_state = None;
- // this.update_visible_entries(None, cx);
- // }
- // })
- // .detach();
-
// cx.observe_global::<FileAssociations, _>(|_, cx| {
// cx.notify();
// })
@@ -1353,14 +1349,7 @@ impl ProjectPanel {
h_stack()
.child(if let Some(icon) = &details.icon {
- div().child(
- // todo!() Marshall: Can we use our `IconElement` component here?
- svg()
- .size(rems(0.9375))
- .flex_none()
- .path(icon.to_string())
- .text_color(cx.theme().colors().icon),
- )
+ div().child(IconElement::from_path(icon.to_string()))
} else {
div()
})
@@ -1468,7 +1457,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));
});
@@ -1577,1296 +1566,1315 @@ impl ClipboardEntry {
}
}
-// todo!()
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle};
-// use pretty_assertions::assert_eq;
-// use project::FakeFs;
-// use serde_json::json;
-// use settings::SettingsStore;
-// use std::{
-// collections::HashSet,
-// path::{Path, PathBuf},
-// sync::atomic::{self, AtomicUsize},
-// };
-// use workspace::{pane, AppState};
-
-// #[gpui::test]
-// async fn test_visible_list(cx: &mut gpui::TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(cx.executor().clone());
-// fs.insert_tree(
-// "/root1",
-// json!({
-// ".dockerignore": "",
-// ".git": {
-// "HEAD": "",
-// },
-// "a": {
-// "0": { "q": "", "r": "", "s": "" },
-// "1": { "t": "", "u": "" },
-// "2": { "v": "", "w": "", "x": "", "y": "" },
-// },
-// "b": {
-// "3": { "Q": "" },
-// "4": { "R": "", "S": "", "T": "", "U": "" },
-// },
-// "C": {
-// "5": {},
-// "6": { "V": "", "W": "" },
-// "7": { "X": "" },
-// "8": { "Y": {}, "Z": "" }
-// }
-// }),
-// )
-// .await;
-// fs.insert_tree(
-// "/root2",
-// json!({
-// "d": {
-// "9": ""
-// },
-// "e": {}
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project.clone(), cx))
-// .root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..50, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// toggle_expand_dir(&panel, "root1/b", cx);
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..50, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b <== selected",
-// " > 3",
-// " > 4",
-// " > C",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// assert_eq!(
-// visible_entries_as_strings(&panel, 6..9, cx),
-// &[
-// //
-// " > C",
-// " .dockerignore",
-// "v root2",
-// ]
-// );
-// }
-
-// #[gpui::test(iterations = 30)]
-// async fn test_editing_files(cx: &mut gpui::TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root1",
-// json!({
-// ".dockerignore": "",
-// ".git": {
-// "HEAD": "",
-// },
-// "a": {
-// "0": { "q": "", "r": "", "s": "" },
-// "1": { "t": "", "u": "" },
-// "2": { "v": "", "w": "", "x": "", "y": "" },
-// },
-// "b": {
-// "3": { "Q": "" },
-// "4": { "R": "", "S": "", "T": "", "U": "" },
-// },
-// "C": {
-// "5": {},
-// "6": { "V": "", "W": "" },
-// "7": { "X": "" },
-// "8": { "Y": {}, "Z": "" }
-// }
-// }),
-// )
-// .await;
-// fs.insert_tree(
-// "/root2",
-// json!({
-// "d": {
-// "9": ""
-// },
-// "e": {}
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-// let workspace = window.root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// select_path(&panel, "root1", cx);
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1 <== selected",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// // Add a file with the root folder selected. The filename editor is placed
-// // before the first file in the root folder.
-// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-// window.read_with(cx, |cx| {
-// let panel = panel.read(cx);
-// assert!(panel.filename_editor.is_focused(cx));
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " [EDITOR: ''] <== selected",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// let confirm = panel.update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
-// panel.confirm(&Confirm, cx).unwrap()
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " [PROCESSING: 'the-new-filename'] <== selected",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// confirm.await.unwrap();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " .dockerignore",
-// " the-new-filename <== selected",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// select_path(&panel, "root1/b", cx);
-// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3",
-// " > 4",
-// " [EDITOR: ''] <== selected",
-// " > C",
-// " .dockerignore",
-// " the-new-filename",
-// ]
-// );
-
-// panel
-// .update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
-// panel.confirm(&Confirm, cx).unwrap()
-// })
-// .await
-// .unwrap();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3",
-// " > 4",
-// " another-filename.txt <== selected",
-// " > C",
-// " .dockerignore",
-// " the-new-filename",
-// ]
-// );
-
-// select_path(&panel, "root1/b/another-filename.txt", cx);
-// panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3",
-// " > 4",
-// " [EDITOR: 'another-filename.txt'] <== selected",
-// " > C",
-// " .dockerignore",
-// " the-new-filename",
-// ]
-// );
-
-// let confirm = panel.update(cx, |panel, cx| {
-// panel.filename_editor.update(cx, |editor, cx| {
-// let file_name_selections = editor.selections.all::<usize>(cx);
-// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
-// let file_name_selection = &file_name_selections[0];
-// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
-// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
-
-// editor.set_text("a-different-filename.tar.gz", cx)
-// });
-// panel.confirm(&Confirm, cx).unwrap()
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3",
-// " > 4",
-// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected",
-// " > C",
-// " .dockerignore",
-// " the-new-filename",
-// ]
-// );
-
-// confirm.await.unwrap();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3",
-// " > 4",
-// " a-different-filename.tar.gz <== selected",
-// " > C",
-// " .dockerignore",
-// " the-new-filename",
-// ]
-// );
-
-// panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3",
-// " > 4",
-// " [EDITOR: 'a-different-filename.tar.gz'] <== selected",
-// " > C",
-// " .dockerignore",
-// " the-new-filename",
-// ]
-// );
-
-// panel.update(cx, |panel, cx| {
-// panel.filename_editor.update(cx, |editor, cx| {
-// let file_name_selections = editor.selections.all::<usize>(cx);
-// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
-// let file_name_selection = &file_name_selections[0];
-// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
-// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot");
-
-// });
-// panel.cancel(&Cancel, cx)
-// });
-
-// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > [EDITOR: ''] <== selected",
-// " > 3",
-// " > 4",
-// " a-different-filename.tar.gz",
-// " > C",
-// " .dockerignore",
-// ]
-// );
-
-// let confirm = panel.update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("new-dir", cx));
-// panel.confirm(&Confirm, cx).unwrap()
-// });
-// panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > [PROCESSING: 'new-dir']",
-// " > 3 <== selected",
-// " > 4",
-// " a-different-filename.tar.gz",
-// " > C",
-// " .dockerignore",
-// ]
-// );
-
-// confirm.await.unwrap();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3 <== selected",
-// " > 4",
-// " > new-dir",
-// " a-different-filename.tar.gz",
-// " > C",
-// " .dockerignore",
-// ]
-// );
-
-// panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > [EDITOR: '3'] <== selected",
-// " > 4",
-// " > new-dir",
-// " a-different-filename.tar.gz",
-// " > C",
-// " .dockerignore",
-// ]
-// );
-
-// // Dismiss the rename editor when it loses focus.
-// workspace.update(cx, |_, cx| cx.focus_self());
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " v b",
-// " > 3 <== selected",
-// " > 4",
-// " > new-dir",
-// " a-different-filename.tar.gz",
-// " > C",
-// " .dockerignore",
-// ]
-// );
-// }
-
-// #[gpui::test(iterations = 30)]
-// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root1",
-// json!({
-// ".dockerignore": "",
-// ".git": {
-// "HEAD": "",
-// },
-// "a": {
-// "0": { "q": "", "r": "", "s": "" },
-// "1": { "t": "", "u": "" },
-// "2": { "v": "", "w": "", "x": "", "y": "" },
-// },
-// "b": {
-// "3": { "Q": "" },
-// "4": { "R": "", "S": "", "T": "", "U": "" },
-// },
-// "C": {
-// "5": {},
-// "6": { "V": "", "W": "" },
-// "7": { "X": "" },
-// "8": { "Y": {}, "Z": "" }
-// }
-// }),
-// )
-// .await;
-// fs.insert_tree(
-// "/root2",
-// json!({
-// "d": {
-// "9": ""
-// },
-// "e": {}
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-// let workspace = window.root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// select_path(&panel, "root1", cx);
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1 <== selected",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// // Add a file with the root folder selected. The filename editor is placed
-// // before the first file in the root folder.
-// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-// window.read_with(cx, |cx| {
-// let panel = panel.read(cx);
-// assert!(panel.filename_editor.is_focused(cx));
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " [EDITOR: ''] <== selected",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// let confirm = panel.update(cx, |panel, cx| {
-// panel.filename_editor.update(cx, |editor, cx| {
-// editor.set_text("/bdir1/dir2/the-new-filename", cx)
-// });
-// panel.confirm(&Confirm, cx).unwrap()
-// });
-
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " > C",
-// " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-
-// confirm.await.unwrap();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..13, cx),
-// &[
-// "v root1",
-// " > .git",
-// " > a",
-// " > b",
-// " v bdir1",
-// " v dir2",
-// " the-new-filename <== selected",
-// " > C",
-// " .dockerignore",
-// "v root2",
-// " > d",
-// " > e",
-// ]
-// );
-// }
-
-// #[gpui::test]
-// async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/root1",
-// json!({
-// "one.two.txt": "",
-// "one.txt": ""
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project.clone(), cx))
-// .root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// panel.update(cx, |panel, cx| {
-// panel.select_next(&Default::default(), cx);
-// panel.select_next(&Default::default(), cx);
-// });
-
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..50, cx),
-// &[
-// //
-// "v root1",
-// " one.two.txt <== selected",
-// " one.txt",
-// ]
-// );
-
-// // Regression test - file name is created correctly when
-// // the copied file's name contains multiple dots.
-// panel.update(cx, |panel, cx| {
-// panel.copy(&Default::default(), cx);
-// panel.paste(&Default::default(), cx);
-// });
-// cx.foreground().run_until_parked();
-
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..50, cx),
-// &[
-// //
-// "v root1",
-// " one.two copy.txt",
-// " one.two.txt <== selected",
-// " one.txt",
-// ]
-// );
-
-// panel.update(cx, |panel, cx| {
-// panel.paste(&Default::default(), cx);
-// });
-// cx.foreground().run_until_parked();
-
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..50, cx),
-// &[
-// //
-// "v root1",
-// " one.two copy 1.txt",
-// " one.two copy.txt",
-// " one.two.txt <== selected",
-// " one.txt",
-// ]
-// );
-// }
-
-// #[gpui::test]
-// async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
-// init_test_with_editor(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.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(fs.clone(), ["/src".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-// let workspace = window.root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// toggle_expand_dir(&panel, "src/test", cx);
-// select_path(&panel, "src/test/first.rs", cx);
-// panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " first.rs <== selected",
-// " second.rs",
-// " third.rs"
-// ]
-// );
-// ensure_single_file_is_opened(window, "test/first.rs", cx);
-
-// submit_deletion(window.into(), &panel, cx);
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " second.rs",
-// " third.rs"
-// ],
-// "Project panel should have no deleted file, no other file is selected in it"
-// );
-// ensure_no_open_items_and_panes(window.into(), &workspace, cx);
-
-// select_path(&panel, "src/test/second.rs", cx);
-// panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " second.rs <== selected",
-// " third.rs"
-// ]
-// );
-// ensure_single_file_is_opened(window, "test/second.rs", cx);
-
-// window.update(cx, |cx| {
-// let active_items = workspace
-// .read(cx)
-// .panes()
-// .iter()
-// .filter_map(|pane| pane.read(cx).active_item())
-// .collect::<Vec<_>>();
-// assert_eq!(active_items.len(), 1);
-// let open_editor = active_items
-// .into_iter()
-// .next()
-// .unwrap()
-// .downcast::<Editor>()
-// .expect("Open item should be an editor");
-// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
-// });
-// submit_deletion(window.into(), &panel, cx);
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v src", " v test", " third.rs"],
-// "Project panel should have no deleted file, with one last file remaining"
-// );
-// ensure_no_open_items_and_panes(window.into(), &workspace, cx);
-// }
-
-// #[gpui::test]
-// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
-// init_test_with_editor(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.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(fs.clone(), ["/src".as_ref()], cx).await;
-// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-// let workspace = window.root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// select_path(&panel, "src/", cx);
-// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v src <== selected", " > test"]
-// );
-// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-// window.read_with(cx, |cx| {
-// let panel = panel.read(cx);
-// assert!(panel.filename_editor.is_focused(cx));
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v src", " > [EDITOR: ''] <== selected", " > test"]
-// );
-// panel.update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("test", cx));
-// assert!(
-// panel.confirm(&Confirm, cx).is_none(),
-// "Should not allow to confirm on conflicting new directory name"
-// )
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v src", " > test"],
-// "File list should be unchanged after failed folder create confirmation"
-// );
-
-// select_path(&panel, "src/test/", cx);
-// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v src", " > test <== selected"]
-// );
-// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-// window.read_with(cx, |cx| {
-// let panel = panel.read(cx);
-// assert!(panel.filename_editor.is_focused(cx));
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " [EDITOR: ''] <== selected",
-// " first.rs",
-// " second.rs",
-// " third.rs"
-// ]
-// );
-// panel.update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("first.rs", cx));
-// assert!(
-// panel.confirm(&Confirm, cx).is_none(),
-// "Should not allow to confirm on conflicting new file name"
-// )
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " first.rs",
-// " second.rs",
-// " third.rs"
-// ],
-// "File list should be unchanged after failed file create confirmation"
-// );
-
-// select_path(&panel, "src/test/first.rs", cx);
-// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " first.rs <== selected",
-// " second.rs",
-// " third.rs"
-// ],
-// );
-// panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-// window.read_with(cx, |cx| {
-// let panel = panel.read(cx);
-// assert!(panel.filename_editor.is_focused(cx));
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " [EDITOR: 'first.rs'] <== selected",
-// " second.rs",
-// " third.rs"
-// ]
-// );
-// panel.update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("second.rs", cx));
-// assert!(
-// panel.confirm(&Confirm, cx).is_none(),
-// "Should not allow to confirm on conflicting file rename"
-// )
-// });
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " first.rs <== selected",
-// " second.rs",
-// " third.rs"
-// ],
-// "File list should be unchanged after failed rename confirmation"
-// );
-// }
-
-// #[gpui::test]
-// async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
-// init_test_with_editor(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.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(fs.clone(), ["/src".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project.clone(), cx))
-// .root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// let new_search_events_count = Arc::new(AtomicUsize::new(0));
-// let _subscription = panel.update(cx, |_, cx| {
-// let subcription_count = Arc::clone(&new_search_events_count);
-// cx.subscribe(&cx.handle(), move |_, _, event, _| {
-// if matches!(event, Event::NewSearchInDirectory { .. }) {
-// subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
-// }
-// })
-// });
-
-// toggle_expand_dir(&panel, "src/test", cx);
-// select_path(&panel, "src/test/first.rs", cx);
-// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test",
-// " first.rs <== selected",
-// " second.rs",
-// " third.rs"
-// ]
-// );
-// panel.update(cx, |panel, cx| {
-// panel.new_search_in_directory(&NewSearchInDirectory, cx)
-// });
-// assert_eq!(
-// new_search_events_count.load(atomic::Ordering::SeqCst),
-// 0,
-// "Should not trigger new search in directory when called on a file"
-// );
-
-// select_path(&panel, "src/test", cx);
-// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v src",
-// " v test <== selected",
-// " first.rs",
-// " second.rs",
-// " third.rs"
-// ]
-// );
-// panel.update(cx, |panel, cx| {
-// panel.new_search_in_directory(&NewSearchInDirectory, cx)
-// });
-// assert_eq!(
-// new_search_events_count.load(atomic::Ordering::SeqCst),
-// 1,
-// "Should trigger new search in directory when called on a directory"
-// );
-// }
-
-// #[gpui::test]
-// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
-// init_test_with_editor(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.insert_tree(
-// "/project_root",
-// json!({
-// "dir_1": {
-// "nested_dir": {
-// "file_a.py": "# File contents",
-// "file_b.py": "# File contents",
-// "file_c.py": "# File contents",
-// },
-// "file_1.py": "# File contents",
-// "file_2.py": "# File contents",
-// "file_3.py": "# File contents",
-// },
-// "dir_2": {
-// "file_1.py": "# File contents",
-// "file_2.py": "# File contents",
-// "file_3.py": "# File contents",
-// }
-// }),
-// )
-// .await;
-
-// let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project.clone(), cx))
-// .root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// panel.update(cx, |panel, cx| {
-// panel.collapse_all_entries(&CollapseAllEntries, cx)
-// });
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v project_root", " > dir_1", " > dir_2",]
-// );
-
-// // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
-// toggle_expand_dir(&panel, "project_root/dir_1", cx);
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &[
-// "v project_root",
-// " v dir_1 <== selected",
-// " > nested_dir",
-// " file_1.py",
-// " file_2.py",
-// " file_3.py",
-// " > dir_2",
-// ]
-// );
-// }
-
-// #[gpui::test]
-// async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
-// init_test(cx);
-
-// let fs = FakeFs::new(cx.background());
-// fs.as_fake().insert_tree("/root", json!({})).await;
-// let project = Project::test(fs, ["/root".as_ref()], cx).await;
-// let workspace = cx
-// .add_window(|cx| Workspace::test_new(project.clone(), cx))
-// .root(cx);
-// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-// // Make a new buffer with no backing file
-// workspace.update(cx, |workspace, cx| {
-// Editor::new_file(workspace, &Default::default(), cx)
-// });
-
-// // "Save as"" the buffer, creating a new backing file for it
-// let task = workspace.update(cx, |workspace, cx| {
-// workspace.save_active_item(workspace::SaveIntent::Save, cx)
-// });
-
-// cx.foreground().run_until_parked();
-// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
-// task.await.unwrap();
-
-// // Rename the file
-// select_path(&panel, "root/new", cx);
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v root", " new <== selected"]
-// );
-// panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-// panel.update(cx, |panel, cx| {
-// panel
-// .filename_editor
-// .update(cx, |editor, cx| editor.set_text("newer", cx));
-// });
-// panel
-// .update(cx, |panel, cx| panel.confirm(&Confirm, cx))
-// .unwrap()
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v root", " newer <== selected"]
-// );
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.save_active_item(workspace::SaveIntent::Save, cx)
-// })
-// .await
-// .unwrap();
-
-// cx.foreground().run_until_parked();
-// // assert that saving the file doesn't restore "new"
-// assert_eq!(
-// visible_entries_as_strings(&panel, 0..10, cx),
-// &["v root", " newer <== selected"]
-// );
-// }
-
-// fn toggle_expand_dir(
-// panel: &View<ProjectPanel>,
-// path: impl AsRef<Path>,
-// cx: &mut TestAppContext,
-// ) {
-// let path = path.as_ref();
-// panel.update(cx, |panel, cx| {
-// for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-// let worktree = worktree.read(cx);
-// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-// let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-// panel.toggle_expanded(entry_id, cx);
-// return;
-// }
-// }
-// panic!("no worktree for path {:?}", path);
-// });
-// }
-
-// fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut TestAppContext) {
-// let path = path.as_ref();
-// panel.update(cx, |panel, cx| {
-// for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-// let worktree = worktree.read(cx);
-// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-// let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-// panel.selection = Some(Selection {
-// worktree_id: worktree.id(),
-// entry_id,
-// });
-// return;
-// }
-// }
-// panic!("no worktree for path {:?}", path);
-// });
-// }
-
-// fn visible_entries_as_strings(
-// panel: &View<ProjectPanel>,
-// range: Range<usize>,
-// cx: &mut TestAppContext,
-// ) -> Vec<String> {
-// let mut result = Vec::new();
-// let mut project_entries = HashSet::new();
-// let mut has_editor = false;
-
-// panel.update(cx, |panel, cx| {
-// panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
-// if details.is_editing {
-// assert!(!has_editor, "duplicate editor entry");
-// has_editor = true;
-// } else {
-// assert!(
-// project_entries.insert(project_entry),
-// "duplicate project entry {:?} {:?}",
-// project_entry,
-// details
-// );
-// }
-
-// let indent = " ".repeat(details.depth);
-// let icon = if details.kind.is_dir() {
-// if details.is_expanded {
-// "v "
-// } else {
-// "> "
-// }
-// } else {
-// " "
-// };
-// let name = if details.is_editing {
-// format!("[EDITOR: '{}']", details.filename)
-// } else if details.is_processing {
-// format!("[PROCESSING: '{}']", details.filename)
-// } else {
-// details.filename.clone()
-// };
-// let selected = if details.is_selected {
-// " <== selected"
-// } else {
-// ""
-// };
-// result.push(format!("{indent}{icon}{name}{selected}"));
-// });
-// });
-
-// result
-// }
-
-// fn init_test(cx: &mut TestAppContext) {
-// cx.foreground().forbid_parking();
-// cx.update(|cx| {
-// cx.set_global(SettingsStore::test(cx));
-// init_settings(cx);
-// theme::init(cx);
-// language::init(cx);
-// editor::init_settings(cx);
-// crate::init((), cx);
-// workspace::init_settings(cx);
-// client::init_settings(cx);
-// Project::init_settings(cx);
-// });
-// }
-
-// fn init_test_with_editor(cx: &mut TestAppContext) {
-// cx.foreground().forbid_parking();
-// cx.update(|cx| {
-// let app_state = AppState::test(cx);
-// theme::init(cx);
-// init_settings(cx);
-// language::init(cx);
-// editor::init(cx);
-// pane::init(cx);
-// crate::init((), cx);
-// workspace::init(app_state.clone(), cx);
-// Project::init_settings(cx);
-// });
-// }
-
-// fn ensure_single_file_is_opened(
-// window: WindowHandle<Workspace>,
-// expected_path: &str,
-// cx: &mut TestAppContext,
-// ) {
-// window.update_root(cx, |workspace, cx| {
-// let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
-// assert_eq!(worktrees.len(), 1);
-// let worktree_id = WorktreeId::from_usize(worktrees[0].id());
-
-// let open_project_paths = workspace
-// .panes()
-// .iter()
-// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
-// .collect::<Vec<_>>();
-// assert_eq!(
-// open_project_paths,
-// vec![ProjectPath {
-// worktree_id,
-// path: Arc::from(Path::new(expected_path))
-// }],
-// "Should have opened file, selected in project panel"
-// );
-// });
-// }
-
-// fn submit_deletion(
-// window: AnyWindowHandle,
-// panel: &View<ProjectPanel>,
-// cx: &mut TestAppContext,
-// ) {
-// assert!(
-// !window.has_pending_prompt(cx),
-// "Should have no prompts before the deletion"
-// );
-// panel.update(cx, |panel, cx| {
-// panel
-// .delete(&Delete, cx)
-// .expect("Deletion start")
-// .detach_and_log_err(cx);
-// });
-// assert!(
-// window.has_pending_prompt(cx),
-// "Should have a prompt after the deletion"
-// );
-// window.simulate_prompt_answer(0, cx);
-// assert!(
-// !window.has_pending_prompt(cx),
-// "Should have no prompts after prompt was replied to"
-// );
-// cx.foreground().run_until_parked();
-// }
-
-// fn ensure_no_open_items_and_panes(
-// window: AnyWindowHandle,
-// workspace: &View<Workspace>,
-// cx: &mut TestAppContext,
-// ) {
-// assert!(
-// !window.has_pending_prompt(cx),
-// "Should have no prompts after deletion operation closes the file"
-// );
-// window.read_with(cx, |cx| {
-// let open_project_paths = workspace
-// .read(cx)
-// .panes()
-// .iter()
-// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
-// .collect::<Vec<_>>();
-// assert!(
-// open_project_paths.is_empty(),
-// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
-// );
-// });
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
+ use pretty_assertions::assert_eq;
+ use project::FakeFs;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::{
+ collections::HashSet,
+ path::{Path, PathBuf},
+ sync::atomic::{self, AtomicUsize},
+ };
+ use workspace::{pane, AppState};
+
+ #[gpui::test]
+ async fn test_visible_list(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
+ }
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "9": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ toggle_expand_dir(&panel, "root1/b", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b <== selected",
+ " > 3",
+ " > 4",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 6..9, cx),
+ &[
+ //
+ " > C",
+ " .dockerignore",
+ "v root2",
+ ]
+ );
+ }
+
+ #[gpui::test(iterations = 30)]
+ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
+ }
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "9": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ select_path(&panel, "root1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1 <== selected",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ // Add a file with the root folder selected. The filename editor is placed
+ // before the first file in the root folder.
+ panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+ panel.update(cx, |panel, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [EDITOR: ''] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
+ panel.confirm_edit(cx).unwrap()
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [PROCESSING: 'the-new-filename'] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ " the-new-filename <== selected",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ select_path(&panel, "root1/b", cx);
+ panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [EDITOR: ''] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ panel
+ .update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
+ panel.confirm_edit(cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " another-filename.txt <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ select_path(&panel, "root1/b/another-filename.txt", cx);
+ panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [EDITOR: 'another-filename.txt'] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ let file_name_selections = editor.selections.all::<usize>(cx);
+ assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+ let file_name_selection = &file_name_selections[0];
+ assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+ assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
+
+ editor.set_text("a-different-filename.tar.gz", cx)
+ });
+ panel.confirm_edit(cx).unwrap()
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [PROCESSING: 'a-different-filename.tar.gz'] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " a-different-filename.tar.gz <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [EDITOR: 'a-different-filename.tar.gz'] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ let file_name_selections = editor.selections.all::<usize>(cx);
+ assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
+ let file_name_selection = &file_name_selections[0];
+ assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
+ assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
+
+ });
+ panel.cancel(&Cancel, cx)
+ });
+
+ panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > [EDITOR: ''] <== selected",
+ " > 3",
+ " > 4",
+ " a-different-filename.tar.gz",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("new-dir", cx));
+ panel.confirm_edit(cx).unwrap()
+ });
+ panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > [PROCESSING: 'new-dir']",
+ " > 3 <== selected",
+ " > 4",
+ " a-different-filename.tar.gz",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3 <== selected",
+ " > 4",
+ " > new-dir",
+ " a-different-filename.tar.gz",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > [EDITOR: '3'] <== selected",
+ " > 4",
+ " > new-dir",
+ " a-different-filename.tar.gz",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+
+ // Dismiss the rename editor when it loses focus.
+ workspace.update(cx, |_, cx| cx.blur()).unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " v b",
+ " > 3 <== selected",
+ " > 4",
+ " > new-dir",
+ " a-different-filename.tar.gz",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+ }
+
+ #[gpui::test(iterations = 10)]
+ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
+ }
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "9": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ select_path(&panel, "root1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1 <== selected",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ // Add a file with the root folder selected. The filename editor is placed
+ // before the first file in the root folder.
+ panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+ panel.update(cx, |panel, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [EDITOR: ''] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ editor.set_text("/bdir1/dir2/the-new-filename", cx)
+ });
+ panel.confirm_edit(cx).unwrap()
+ });
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..13, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " v bdir1",
+ " v dir2",
+ " the-new-filename <== selected",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ "one.two.txt": "",
+ "one.txt": ""
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ panel.update(cx, |panel, cx| {
+ panel.select_next(&Default::default(), cx);
+ panel.select_next(&Default::default(), cx);
+ });
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ //
+ "v root1",
+ " one.two.txt <== selected",
+ " one.txt",
+ ]
+ );
+
+ // Regression test - file name is created correctly when
+ // the copied file's name contains multiple dots.
+ panel.update(cx, |panel, cx| {
+ panel.copy(&Default::default(), cx);
+ panel.paste(&Default::default(), cx);
+ });
+ cx.executor().run_until_parked();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ //
+ "v root1",
+ " one.two copy.txt",
+ " one.two.txt <== selected",
+ " one.txt",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| {
+ panel.paste(&Default::default(), cx);
+ });
+ cx.executor().run_until_parked();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..50, cx),
+ &[
+ //
+ "v root1",
+ " one.two copy 1.txt",
+ " one.two copy.txt",
+ " one.two.txt <== selected",
+ " one.txt",
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.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(fs.clone(), ["/src".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ toggle_expand_dir(&panel, "src/test", cx);
+ select_path(&panel, "src/test/first.rs", cx);
+ panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs <== selected",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
+
+ submit_deletion(&panel, cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " second.rs",
+ " third.rs"
+ ],
+ "Project panel should have no deleted file, no other file is selected in it"
+ );
+ ensure_no_open_items_and_panes(&workspace, cx);
+
+ select_path(&panel, "src/test/second.rs", cx);
+ panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " second.rs <== selected",
+ " third.rs"
+ ]
+ );
+ ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
+
+ workspace
+ .update(cx, |workspace, cx| {
+ let active_items = workspace
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item())
+ .collect::<Vec<_>>();
+ assert_eq!(active_items.len(), 1);
+ let open_editor = active_items
+ .into_iter()
+ .next()
+ .unwrap()
+ .downcast::<Editor>()
+ .expect("Open item should be an editor");
+ open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
+ })
+ .unwrap();
+ submit_deletion(&panel, cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v src", " v test", " third.rs"],
+ "Project panel should have no deleted file, with one last file remaining"
+ );
+ ensure_no_open_items_and_panes(&workspace, cx);
+ }
+
+ #[gpui::test]
+ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.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(fs.clone(), ["/src".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ select_path(&panel, "src/", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ //
+ "v src <== selected",
+ " > test"
+ ]
+ );
+ panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
+ panel.update(cx, |panel, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ //
+ "v src",
+ " > [EDITOR: ''] <== selected",
+ " > test"
+ ]
+ );
+ panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("test", cx));
+ assert!(
+ panel.confirm_edit(cx).is_none(),
+ "Should not allow to confirm on conflicting new directory name"
+ )
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ //
+ "v src",
+ " > test"
+ ],
+ "File list should be unchanged after failed folder create confirmation"
+ );
+
+ select_path(&panel, "src/test/", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ //
+ "v src",
+ " > test <== selected"
+ ]
+ );
+ panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+ panel.update(cx, |panel, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " [EDITOR: ''] <== selected",
+ " first.rs",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("first.rs", cx));
+ assert!(
+ panel.confirm_edit(cx).is_none(),
+ "Should not allow to confirm on conflicting new file name"
+ )
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs",
+ " second.rs",
+ " third.rs"
+ ],
+ "File list should be unchanged after failed file create confirmation"
+ );
+
+ select_path(&panel, "src/test/first.rs", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs <== selected",
+ " second.rs",
+ " third.rs"
+ ],
+ );
+ panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+ panel.update(cx, |panel, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " [EDITOR: 'first.rs'] <== selected",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("second.rs", cx));
+ assert!(
+ panel.confirm_edit(cx).is_none(),
+ "Should not allow to confirm on conflicting file rename"
+ )
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs <== selected",
+ " second.rs",
+ " third.rs"
+ ],
+ "File list should be unchanged after failed rename confirmation"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.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(fs.clone(), ["/src".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ let new_search_events_count = Arc::new(AtomicUsize::new(0));
+ let _subscription = panel.update(cx, |_, cx| {
+ let subcription_count = Arc::clone(&new_search_events_count);
+ let view = cx.view().clone();
+ cx.subscribe(&view, move |_, _, event, _| {
+ if matches!(event, Event::NewSearchInDirectory { .. }) {
+ subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
+ }
+ })
+ });
+
+ toggle_expand_dir(&panel, "src/test", cx);
+ select_path(&panel, "src/test/first.rs", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test",
+ " first.rs <== selected",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ panel.update(cx, |panel, cx| {
+ panel.new_search_in_directory(&NewSearchInDirectory, cx)
+ });
+ assert_eq!(
+ new_search_events_count.load(atomic::Ordering::SeqCst),
+ 0,
+ "Should not trigger new search in directory when called on a file"
+ );
+
+ select_path(&panel, "src/test", cx);
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v src",
+ " v test <== selected",
+ " first.rs",
+ " second.rs",
+ " third.rs"
+ ]
+ );
+ panel.update(cx, |panel, cx| {
+ panel.new_search_in_directory(&NewSearchInDirectory, cx)
+ });
+ assert_eq!(
+ new_search_events_count.load(atomic::Ordering::SeqCst),
+ 1,
+ "Should trigger new search in directory when called on a directory"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/project_root",
+ json!({
+ "dir_1": {
+ "nested_dir": {
+ "file_a.py": "# File contents",
+ "file_b.py": "# File contents",
+ "file_c.py": "# File contents",
+ },
+ "file_1.py": "# File contents",
+ "file_2.py": "# File contents",
+ "file_3.py": "# File contents",
+ },
+ "dir_2": {
+ "file_1.py": "# File contents",
+ "file_2.py": "# File contents",
+ "file_3.py": "# File contents",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ panel.update(cx, |panel, cx| {
+ panel.collapse_all_entries(&CollapseAllEntries, cx)
+ });
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v project_root", " > dir_1", " > dir_2",]
+ );
+
+ // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
+ toggle_expand_dir(&panel, "project_root/dir_1", cx);
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v project_root",
+ " v dir_1 <== selected",
+ " > nested_dir",
+ " file_1.py",
+ " file_2.py",
+ " file_3.py",
+ " > dir_2",
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.as_fake().insert_tree("/root", json!({})).await;
+ let project = Project::test(fs, ["/root".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+ let panel = workspace
+ .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
+ .unwrap();
+
+ // Make a new buffer with no backing file
+ workspace
+ .update(cx, |workspace, cx| {
+ Editor::new_file(workspace, &Default::default(), cx)
+ })
+ .unwrap();
+
+ // "Save as"" the buffer, creating a new backing file for it
+ let save_task = workspace
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(workspace::SaveIntent::Save, cx)
+ })
+ .unwrap();
+
+ cx.executor().run_until_parked();
+ cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
+ save_task.await.unwrap();
+
+ // Rename the file
+ select_path(&panel, "root/new", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v root", " new <== selected"]
+ );
+ panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+ panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("newer", cx));
+ });
+ panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
+
+ cx.executor().run_until_parked();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v root", " newer <== selected"]
+ );
+
+ workspace
+ .update(cx, |workspace, cx| {
+ workspace.save_active_item(workspace::SaveIntent::Save, cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+
+ cx.executor().run_until_parked();
+ // assert that saving the file doesn't restore "new"
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["v root", " newer <== selected"]
+ );
+ }
+
+ fn toggle_expand_dir(
+ panel: &View<ProjectPanel>,
+ path: impl AsRef<Path>,
+ cx: &mut VisualTestContext,
+ ) {
+ let path = path.as_ref();
+ panel.update(cx, |panel, cx| {
+ for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
+ let worktree = worktree.read(cx);
+ if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+ let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+ panel.toggle_expanded(entry_id, cx);
+ return;
+ }
+ }
+ panic!("no worktree for path {:?}", path);
+ });
+ }
+
+ fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
+ let path = path.as_ref();
+ panel.update(cx, |panel, cx| {
+ for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
+ let worktree = worktree.read(cx);
+ if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+ let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+ panel.selection = Some(Selection {
+ worktree_id: worktree.id(),
+ entry_id,
+ });
+ return;
+ }
+ }
+ panic!("no worktree for path {:?}", path);
+ });
+ }
+
+ fn visible_entries_as_strings(
+ panel: &View<ProjectPanel>,
+ range: Range<usize>,
+ cx: &mut VisualTestContext,
+ ) -> Vec<String> {
+ let mut result = Vec::new();
+ let mut project_entries = HashSet::new();
+ let mut has_editor = false;
+
+ panel.update(cx, |panel, cx| {
+ panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
+ if details.is_editing {
+ assert!(!has_editor, "duplicate editor entry");
+ has_editor = true;
+ } else {
+ assert!(
+ project_entries.insert(project_entry),
+ "duplicate project entry {:?} {:?}",
+ project_entry,
+ details
+ );
+ }
+
+ let indent = " ".repeat(details.depth);
+ let icon = if details.kind.is_dir() {
+ if details.is_expanded {
+ "v "
+ } else {
+ "> "
+ }
+ } else {
+ " "
+ };
+ let name = if details.is_editing {
+ format!("[EDITOR: '{}']", details.filename)
+ } else if details.is_processing {
+ format!("[PROCESSING: '{}']", details.filename)
+ } else {
+ details.filename.clone()
+ };
+ let selected = if details.is_selected {
+ " <== selected"
+ } else {
+ ""
+ };
+ result.push(format!("{indent}{icon}{name}{selected}"));
+ });
+ });
+
+ result
+ }
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ init_settings(cx);
+ theme::init(cx);
+ language::init(cx);
+ editor::init_settings(cx);
+ crate::init((), cx);
+ workspace::init_settings(cx);
+ client::init_settings(cx);
+ Project::init_settings(cx);
+ });
+ }
+
+ fn init_test_with_editor(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let app_state = AppState::test(cx);
+ theme::init(cx);
+ init_settings(cx);
+ language::init(cx);
+ editor::init(cx);
+ pane::init(cx);
+ crate::init((), cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+ }
+
+ fn ensure_single_file_is_opened(
+ window: &WindowHandle<Workspace>,
+ expected_path: &str,
+ cx: &mut TestAppContext,
+ ) {
+ window
+ .update(cx, |workspace, cx| {
+ let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ let worktree_id = worktrees[0].read(cx).id();
+
+ let open_project_paths = workspace
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+ .collect::<Vec<_>>();
+ assert_eq!(
+ open_project_paths,
+ vec![ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new(expected_path))
+ }],
+ "Should have opened file, selected in project panel"
+ );
+ })
+ .unwrap();
+ }
+
+ fn submit_deletion(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
+ assert!(
+ !cx.has_pending_prompt(),
+ "Should have no prompts before the deletion"
+ );
+ panel.update(cx, |panel, cx| panel.delete(&Delete, cx));
+ assert!(
+ cx.has_pending_prompt(),
+ "Should have a prompt after the deletion"
+ );
+ cx.simulate_prompt_answer(0);
+ assert!(
+ !cx.has_pending_prompt(),
+ "Should have no prompts after prompt was replied to"
+ );
+ cx.executor().run_until_parked();
+ }
+
+ fn ensure_no_open_items_and_panes(
+ workspace: &WindowHandle<Workspace>,
+ cx: &mut VisualTestContext,
+ ) {
+ assert!(
+ !cx.has_pending_prompt(),
+ "Should have no prompts after deletion operation closes the file"
+ );
+ workspace
+ .read_with(cx, |workspace, cx| {
+ let open_project_paths = workspace
+ .panes()
+ .iter()
+ .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
+ .collect::<Vec<_>>();
+ assert!(
+ open_project_paths.is_empty(),
+ "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
+ );
+ })
+ .unwrap();
+ }
+}