Merge branch 'main' into element-types

Mikayla created

Change summary

Cargo.lock                                          |   26 
crates/editor2/src/editor.rs                        |    8 
crates/editor2/src/element.rs                       |    1 
crates/file_finder2/Cargo.toml                      |   37 
crates/file_finder2/src/file_finder.rs              | 1977 +++++++++++
crates/gpui2/src/app.rs                             |   21 
crates/gpui2/src/app/entity_map.rs                  |    2 
crates/gpui2/src/app/test_context.rs                |   51 
crates/gpui2/src/elements/uniform_list.rs           |    2 
crates/gpui2/src/platform/test/platform.rs          |   86 
crates/gpui2/src/platform/test/window.rs            |   28 
crates/live_kit_client2/examples/test_app.rs        |    2 
crates/node_runtime/src/node_runtime.rs             |   29 
crates/picker2/src/picker2.rs                       |   16 
crates/project_panel2/src/project_panel.rs          | 2644 +++++++-------
crates/theme2/src/one_themes.rs                     |    1 
crates/theme2/src/settings.rs                       |    7 
crates/ui2/src/components/icon.rs                   |   14 
crates/ui2/src/components/tooltip.rs                |    8 
crates/ui2/src/to_extract/workspace.rs              |    3 
crates/workspace2/src/dock.rs                       |   14 
crates/workspace2/src/modal_layer.rs                |    8 
crates/workspace2/src/pane/dragged_item_receiver.rs |    2 
crates/workspace2/src/workspace2.rs                 |  191 
crates/zed2/Cargo.toml                              |    2 
crates/zed2/src/main.rs                             |    2 
crates/zed2/src/zed2.rs                             |    1 
27 files changed, 3,713 insertions(+), 1,470 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3061,6 +3061,31 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "file_finder2"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "ctor",
+ "editor2",
+ "env_logger 0.9.3",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "menu2",
+ "picker2",
+ "postage",
+ "project2",
+ "serde",
+ "serde_json",
+ "settings2",
+ "text2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "filetime"
 version = "0.2.22"
@@ -11424,6 +11449,7 @@ dependencies = [
  "editor2",
  "env_logger 0.9.3",
  "feature_flags2",
+ "file_finder2",
  "fs2",
  "fsevent",
  "futures 0.3.28",

crates/editor2/src/editor.rs 🔗

@@ -9168,6 +9168,10 @@ impl Editor {
         cx.focus(&self.focus_handle)
     }
 
+    pub fn is_focused(&self, cx: &WindowContext) -> bool {
+        self.focus_handle.is_focused(cx)
+    }
+
     fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
         if self.focus_handle.is_focused(cx) {
             // todo!()
@@ -9379,8 +9383,8 @@ impl Render for Editor {
             EditorMode::SingleLine => {
                 TextStyle {
                     color: cx.theme().colors().text,
-                    font_family: "Zed Sans".into(), // todo!()
-                    font_features: FontFeatures::default(),
+                    font_family: settings.ui_font.family.clone(), // todo!()
+                    font_features: settings.ui_font.features,
                     font_size: rems(0.875).into(),
                     font_weight: FontWeight::NORMAL,
                     font_style: FontStyle::Normal,

crates/editor2/src/element.rs 🔗

@@ -1448,6 +1448,7 @@ impl EditorElement {
 
         let snapshot = editor.snapshot(cx);
         let style = self.style.clone();
+
         let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let line_height = style.text.line_height_in_pixels(cx.rem_size());

crates/file_finder2/Cargo.toml 🔗

@@ -0,0 +1,37 @@
+[package]
+name = "file_finder2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/file_finder.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+collections = { path = "../collections" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+text = { package = "text2", path = "../text2" }
+util = { path = "../util" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+postage.workspace = true
+serde.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
+
+serde_json.workspace = true
+ctor.workspace = true
+env_logger.workspace = true

crates/file_finder2/src/file_finder.rs 🔗

@@ -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)
+//     }
+// }

crates/gpui2/src/app.rs 🔗

@@ -234,10 +234,10 @@ impl AppContext {
             app_version: platform.app_version().ok(),
         };
 
-        Rc::new_cyclic(|this| AppCell {
+        let app = Rc::new_cyclic(|this| AppCell {
             app: RefCell::new(AppContext {
                 this: this.clone(),
-                platform,
+                platform: platform.clone(),
                 app_metadata,
                 text_system,
                 flushing_effects: false,
@@ -269,12 +269,21 @@ impl AppContext {
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
             }),
-        })
+        });
+
+        platform.on_quit(Box::new({
+            let cx = app.clone();
+            move || {
+                cx.borrow_mut().shutdown();
+            }
+        }));
+
+        app
     }
 
     /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
     /// will be given 100ms to complete before exiting.
-    pub fn quit(&mut self) {
+    pub fn shutdown(&mut self) {
         let mut futures = Vec::new();
 
         for observer in self.quit_observers.remove(&()) {
@@ -292,8 +301,10 @@ impl AppContext {
         {
             log::error!("timed out waiting on app_will_quit");
         }
+    }
 
-        self.globals_by_type.clear();
+    pub fn quit(&mut self) {
+        self.platform.quit();
     }
 
     pub fn app_metadata(&self) -> AppMetadata {

crates/gpui2/src/app/entity_map.rs 🔗

@@ -26,7 +26,7 @@ impl EntityId {
 
 impl Display for EntityId {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self)
+        write!(f, "{}", self.as_u64())
     }
 }
 

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
-    div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor,
-    Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model,
-    ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext,
-    VisualContext, WindowContext, WindowHandle, WindowOptions,
+    div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
+    BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
+    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -14,6 +14,7 @@ pub struct TestAppContext {
     pub background_executor: BackgroundExecutor,
     pub foreground_executor: ForegroundExecutor,
     pub dispatcher: TestDispatcher,
+    pub test_platform: Rc<TestPlatform>,
 }
 
 impl Context for TestAppContext {
@@ -77,17 +78,16 @@ impl TestAppContext {
         let arc_dispatcher = Arc::new(dispatcher.clone());
         let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
         let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
-        let platform = Rc::new(TestPlatform::new(
-            background_executor.clone(),
-            foreground_executor.clone(),
-        ));
+        let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
         let asset_source = Arc::new(());
         let http_client = util::http::FakeHttpClient::with_404_response();
+
         Self {
-            app: AppContext::new(platform, asset_source, http_client),
+            app: AppContext::new(platform.clone(), asset_source, http_client),
             background_executor,
             foreground_executor,
             dispatcher: dispatcher.clone(),
+            test_platform: platform,
         }
     }
 
@@ -96,7 +96,7 @@ impl TestAppContext {
     }
 
     pub fn quit(&self) {
-        self.app.borrow_mut().quit();
+        self.app.borrow_mut().shutdown();
     }
 
     pub fn refresh(&mut self) -> Result<()> {
@@ -152,6 +152,21 @@ impl TestAppContext {
         (view, VisualTestContext::from_window(*window.deref(), self))
     }
 
+    pub fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        self.test_platform.simulate_new_path_selection(select_path);
+    }
+
+    pub fn simulate_prompt_answer(&self, button_ix: usize) {
+        self.test_platform.simulate_prompt_answer(button_ix);
+    }
+
+    pub fn has_pending_prompt(&self) -> bool {
+        self.test_platform.has_pending_prompt()
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -199,6 +214,15 @@ impl TestAppContext {
         }
     }
 
+    pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
+    where
+        A: Action,
+    {
+        window
+            .update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
+            .unwrap()
+    }
+
     pub fn dispatch_keystroke(
         &mut self,
         window: AnyWindowHandle,
@@ -376,6 +400,13 @@ impl<'a> VisualTestContext<'a> {
     pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
         Self { cx, window }
     }
+
+    pub fn dispatch_action<A>(&mut self, action: A)
+    where
+        A: Action,
+    {
+        self.cx.dispatch_action(self.window, action)
+    }
 }
 
 impl<'a> Context for VisualTestContext<'a> {

crates/gpui2/src/elements/uniform_list.rs 🔗

@@ -14,7 +14,7 @@ use taffy::style::Overflow;
 pub fn uniform_list<I, V, C>(
     id: I,
     item_count: usize,
-    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> SmallVec<[C; 64]>,
+    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
 ) -> UniformList<V>
 where
     I: Into<ElementId>,

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -3,8 +3,15 @@ use crate::{
     PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
 };
 use anyhow::{anyhow, Result};
+use collections::VecDeque;
+use futures::channel::oneshot;
 use parking_lot::Mutex;
-use std::{rc::Rc, sync::Arc};
+use std::{
+    cell::RefCell,
+    path::PathBuf,
+    rc::{Rc, Weak},
+    sync::Arc,
+};
 
 pub struct TestPlatform {
     background_executor: BackgroundExecutor,
@@ -13,18 +20,60 @@ pub struct TestPlatform {
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
     active_display: Rc<dyn PlatformDisplay>,
     active_cursor: Mutex<CursorStyle>,
+    pub(crate) prompts: RefCell<TestPrompts>,
+    weak: Weak<Self>,
+}
+
+#[derive(Default)]
+pub(crate) struct TestPrompts {
+    multiple_choice: VecDeque<oneshot::Sender<usize>>,
+    new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
 }
 
 impl TestPlatform {
-    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self {
-        TestPlatform {
+    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
+        Rc::new_cyclic(|weak| TestPlatform {
             background_executor: executor,
             foreground_executor,
-
+            prompts: Default::default(),
             active_cursor: Default::default(),
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
-        }
+            weak: weak.clone(),
+        })
+    }
+
+    pub(crate) fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        let (path, tx) = self
+            .prompts
+            .borrow_mut()
+            .new_path
+            .pop_front()
+            .expect("no pending new path prompt");
+        tx.send(select_path(&path)).ok();
+    }
+
+    pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
+        let tx = self
+            .prompts
+            .borrow_mut()
+            .multiple_choice
+            .pop_front()
+            .expect("no pending multiple choice prompt");
+        tx.send(response_ix).ok();
+    }
+
+    pub(crate) fn has_pending_prompt(&self) -> bool {
+        !self.prompts.borrow().multiple_choice.is_empty()
+    }
+
+    pub(crate) fn prompt(&self) -> oneshot::Receiver<usize> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts.borrow_mut().multiple_choice.push_back(tx);
+        rx
     }
 }
 
@@ -46,9 +95,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn quit(&self) {
-        unimplemented!()
-    }
+    fn quit(&self) {}
 
     fn restart(&self) {
         unimplemented!()
@@ -88,7 +135,11 @@ impl Platform for TestPlatform {
         options: WindowOptions,
     ) -> Box<dyn crate::PlatformWindow> {
         *self.active_window.lock() = Some(handle);
-        Box::new(TestWindow::new(options, self.active_display.clone()))
+        Box::new(TestWindow::new(
+            options,
+            self.weak.clone(),
+            self.active_display.clone(),
+        ))
     }
 
     fn set_display_link_output_callback(
@@ -118,15 +169,20 @@ impl Platform for TestPlatform {
     fn prompt_for_paths(
         &self,
         _options: crate::PathPromptOptions,
-    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+    ) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
         unimplemented!()
     }
 
     fn prompt_for_new_path(
         &self,
-        _directory: &std::path::Path,
-    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
-        unimplemented!()
+        directory: &std::path::Path,
+    ) -> oneshot::Receiver<Option<std::path::PathBuf>> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts
+            .borrow_mut()
+            .new_path
+            .push_back((directory.to_path_buf(), tx));
+        rx
     }
 
     fn reveal_path(&self, _path: &std::path::Path) {
@@ -141,9 +197,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn on_quit(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
+    fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
 
     fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
         unimplemented!()

crates/gpui2/src/platform/test/window.rs 🔗

@@ -1,15 +1,13 @@
-use std::{
-    rc::Rc,
-    sync::{self, Arc},
+use crate::{
+    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
+    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId,
+    WindowAppearance, WindowBounds, WindowOptions,
 };
-
 use collections::HashMap;
 use parking_lot::Mutex;
-
-use crate::{
-    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
-    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance,
-    WindowBounds, WindowOptions,
+use std::{
+    rc::{Rc, Weak},
+    sync::{self, Arc},
 };
 
 #[derive(Default)]
@@ -25,16 +23,22 @@ pub struct TestWindow {
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
     input_handler: Option<Box<dyn PlatformInputHandler>>,
-
     handlers: Mutex<Handlers>,
+    platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
 }
+
 impl TestWindow {
-    pub fn new(options: WindowOptions, display: Rc<dyn PlatformDisplay>) -> Self {
+    pub fn new(
+        options: WindowOptions,
+        platform: Weak<TestPlatform>,
+        display: Rc<dyn PlatformDisplay>,
+    ) -> Self {
         Self {
             bounds: options.bounds,
             current_scene: Default::default(),
             display,
+            platform,
             input_handler: None,
             sprite_atlas: Arc::new(TestAtlas::new()),
             handlers: Default::default(),
@@ -89,7 +93,7 @@ impl PlatformWindow for TestWindow {
         _msg: &str,
         _answers: &[&str],
     ) -> futures::channel::oneshot::Receiver<usize> {
-        todo!()
+        self.platform.upgrade().expect("platform dropped").prompt()
     }
 
     fn activate(&self) {

crates/node_runtime/src/node_runtime.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use serde::Deserialize;
-use smol::{fs, io::BufReader, process::Command};
+use smol::{fs, io::BufReader, lock::Mutex, process::Command};
 use std::process::{Output, Stdio};
 use std::{
     env::consts,
@@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync {
 
 pub struct RealNodeRuntime {
     http: Arc<dyn HttpClient>,
+    installation_lock: Mutex<()>,
 }
 
 impl RealNodeRuntime {
     pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> {
-        Arc::new(RealNodeRuntime { http })
+        Arc::new(RealNodeRuntime {
+            http,
+            installation_lock: Mutex::new(()),
+        })
     }
 
     async fn install_if_needed(&self) -> Result<PathBuf> {
+        let _lock = self.installation_lock.lock().await;
         log::info!("Node runtime install_if_needed");
 
         let arch = match consts::ARCH {
@@ -73,6 +78,9 @@ impl RealNodeRuntime {
             .stdin(Stdio::null())
             .stdout(Stdio::null())
             .stderr(Stdio::null())
+            .args(["--cache".into(), node_dir.join("cache")])
+            .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
+            .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
             .status()
             .await;
         let valid = matches!(result, Ok(status) if status.success());
@@ -96,6 +104,11 @@ impl RealNodeRuntime {
             archive.unpack(&node_containing_dir).await?;
         }
 
+        // Note: Not in the `if !valid {}` so we can populate these for existing installations
+        _ = fs::create_dir(node_dir.join("cache")).await;
+        _ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
+        _ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
+
         anyhow::Ok(node_dir)
     }
 }
@@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime {
 
             let mut command = Command::new(node_binary);
             command.env("PATH", env_path);
-            command.arg(npm_file).arg(subcommand).args(args);
+            command.arg(npm_file).arg(subcommand);
+            command.args(["--cache".into(), installation_path.join("cache")]);
+            command.args([
+                "--userconfig".into(),
+                installation_path.join("blank_user_npmrc"),
+            ]);
+            command.args([
+                "--globalconfig".into(),
+                installation_path.join("blank_global_npmrc"),
+            ]);
+            command.args(args);
 
             if let Some(directory) = directory {
                 command.current_dir(directory);

crates/picker2/src/picker2.rs 🔗

@@ -58,7 +58,7 @@ impl<D: PickerDelegate> Picker<D> {
         self.editor.update(cx, |editor, cx| editor.focus(cx));
     }
 
-    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+    pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
         let count = self.delegate.match_count();
         if count > 0 {
             let index = self.delegate.selected_index();
@@ -98,6 +98,15 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
+    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        let index = self.delegate.selected_index();
+        let new_index = if index + 1 == count { 0 } else { index + 1 };
+        self.delegate.set_selected_index(new_index, cx);
+        self.scroll_handle.scroll_to_item(new_index);
+        cx.notify();
+    }
+
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         self.delegate.dismissed(cx);
     }
@@ -137,6 +146,11 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
+    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
+        let query = self.editor.read(cx).text(cx);
+        self.update_matches(query, cx);
+    }
+
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
         self.matches_updated(cx);

crates/project_panel2/src/project_panel.rs 🔗

@@ -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();
+    }
+}

crates/theme2/src/one_themes.rs 🔗

@@ -35,6 +35,7 @@ pub(crate) fn one_dark() -> Theme {
         id: "one_dark".to_string(),
         name: "One Dark".into(),
         appearance: Appearance::Dark,
+
         styles: ThemeStyles {
             system: SystemColors::default(),
             colors: ThemeColors {

crates/theme2/src/settings.rs 🔗

@@ -19,6 +19,7 @@ const MIN_LINE_HEIGHT: f32 = 1.0;
 #[derive(Clone)]
 pub struct ThemeSettings {
     pub ui_font_size: Pixels,
+    pub ui_font: Font,
     pub buffer_font: Font,
     pub buffer_font_size: Pixels,
     pub buffer_line_height: BufferLineHeight,
@@ -120,6 +121,12 @@ impl settings::Settings for ThemeSettings {
 
         let mut this = Self {
             ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+            ui_font: Font {
+                family: "Helvetica".into(),
+                features: Default::default(),
+                weight: Default::default(),
+                style: Default::default(),
+            },
             buffer_font: Font {
                 family: defaults.buffer_font_family.clone().unwrap().into(),
                 features: defaults.buffer_font_features.clone().unwrap(),

crates/ui2/src/components/icon.rs 🔗

@@ -129,7 +129,7 @@ impl Icon {
 
 #[derive(Component)]
 pub struct IconElement {
-    icon: Icon,
+    path: SharedString,
     color: TextColor,
     size: IconSize,
 }
@@ -137,7 +137,15 @@ pub struct IconElement {
 impl IconElement {
     pub fn new(icon: Icon) -> Self {
         Self {
-            icon,
+            path: icon.path().into(),
+            color: TextColor::default(),
+            size: IconSize::default(),
+        }
+    }
+
+    pub fn from_path(path: impl Into<SharedString>) -> Self {
+        Self {
+            path: path.into(),
             color: TextColor::default(),
             size: IconSize::default(),
         }
@@ -162,7 +170,7 @@ impl IconElement {
         svg()
             .size(svg_size)
             .flex_none()
-            .path(self.icon.path())
+            .path(self.path)
             .text_color(self.color.color(cx))
     }
 }

crates/ui2/src/components/tooltip.rs 🔗

@@ -1,5 +1,6 @@
-use gpui::{Div, ParentComponent, Render, SharedString, Styled, ViewContext};
-use theme2::ActiveTheme;
+use gpui::{Div, Render};
+use settings2::Settings;
+use theme2::{ActiveTheme, ThemeSettings};
 
 use crate::prelude::*;
 use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
@@ -34,9 +35,10 @@ impl Render for TextTooltip {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
         v_stack()
             .elevation_2(cx)
-            .font("Zed Sans")
+            .font(ui_font)
             .text_ui_sm()
             .text_color(cx.theme().colors().text)
             .py_1()

crates/ui2/src/to_extract/workspace.rs 🔗

@@ -206,13 +206,14 @@ impl Render for Workspace {
             .child(self.editor_1.clone())],
             SplitDirection::Horizontal,
         );
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
 
         div()
             .relative()
             .size_full()
             .flex()
             .flex_col()
-            .font("Zed Sans")
+            .font(ui_font)
             .gap_0()
             .justify_start()
             .items_start()

crates/workspace2/src/dock.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, FocusHandle,
-    ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView, WindowContext,
+    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView,
+    WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -429,7 +430,14 @@ impl Render for Dock {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         if let Some(entry) = self.visible_entry() {
-            div().size_full().child(entry.panel.to_any())
+            let size = entry.panel.size(cx);
+
+            div()
+                .map(|this| match self.position().axis() {
+                    Axis::Horizontal => this.w(px(size)).h_full(),
+                    Axis::Vertical => this.h(px(size)).w_full(),
+                })
+                .child(entry.panel.to_any())
         } else {
             div()
         }

crates/workspace2/src/modal_layer.rs 🔗

@@ -71,6 +71,14 @@ impl ModalLayer {
 
         cx.notify();
     }
+
+    pub fn current_modal<V>(&self) -> Option<View<V>>
+    where
+        V: 'static,
+    {
+        let active_modal = self.active_modal.as_ref()?;
+        active_modal.modal.clone().downcast::<V>().ok()
+    }
 }
 
 impl Render for ModalLayer {

crates/workspace2/src/pane/dragged_item_receiver.rs 🔗

@@ -2,7 +2,7 @@ use super::DraggedItem;
 use crate::{Pane, SplitDirection, Workspace};
 use gpui::{
     color::Color,
-    elements::{Canvas, MouseEventHandler, ParentElement, Stack},
+    elements::{Canvas, MouseEventHandler, ParentComponent, Stack},
     geometry::{rect::RectF, vector::Vector2F},
     platform::MouseButton,
     scene::MouseUp,

crates/workspace2/src/workspace2.rs 🔗

@@ -66,9 +66,10 @@ use std::{
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
-use theme2::ActiveTheme;
+use theme2::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip};
+use ui::TextColor;
+use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
 use util::ResultExt;
 use uuid::Uuid;
 pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
@@ -1765,50 +1766,50 @@ impl Workspace {
         })
     }
 
-    //     pub fn open_abs_path(
-    //         &mut self,
-    //         abs_path: PathBuf,
-    //         visible: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             let open_paths_task_result = workspace
-    //                 .update(&mut cx, |workspace, cx| {
-    //                     workspace.open_paths(vec![abs_path.clone()], visible, cx)
-    //                 })
-    //                 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
-    //                 .await;
-    //             anyhow::ensure!(
-    //                 open_paths_task_result.len() == 1,
-    //                 "open abs path {abs_path:?} task returned incorrect number of results"
-    //             );
-    //             match open_paths_task_result
-    //                 .into_iter()
-    //                 .next()
-    //                 .expect("ensured single task result")
-    //             {
-    //                 Some(open_result) => {
-    //                     open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
-    //                 }
-    //                 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
-    //             }
-    //         })
-    //     }
+    pub fn open_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        cx.spawn(|workspace, mut cx| async move {
+            let open_paths_task_result = workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.open_paths(vec![abs_path.clone()], visible, cx)
+                })
+                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
+                .await;
+            anyhow::ensure!(
+                open_paths_task_result.len() == 1,
+                "open abs path {abs_path:?} task returned incorrect number of results"
+            );
+            match open_paths_task_result
+                .into_iter()
+                .next()
+                .expect("ensured single task result")
+            {
+                Some(open_result) => {
+                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
+                }
+                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
+            }
+        })
+    }
 
-    //     pub fn split_abs_path(
-    //         &mut self,
-    //         abs_path: PathBuf,
-    //         visible: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
-    //         let project_path_task =
-    //             Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
-    //         cx.spawn(|this, mut cx| async move {
-    //             let (_, path) = project_path_task.await?;
-    //             this.update(&mut cx, |this, cx| this.split_path(path, cx))?
-    //                 .await
-    //         })
-    //     }
+    pub fn split_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        let project_path_task =
+            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
+        cx.spawn(|this, mut cx| async move {
+            let (_, path) = project_path_task.await?;
+            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
+                .await
+        })
+    }
 
     pub fn open_path(
         &mut self,
@@ -1835,37 +1836,37 @@ impl Workspace {
         })
     }
 
-    //     pub fn split_path(
-    //         &mut self,
-    //         path: impl Into<ProjectPath>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
-    //         let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
-    //             self.panes
-    //                 .first()
-    //                 .expect("There must be an active pane")
-    //                 .downgrade()
-    //         });
+    pub fn split_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
+            self.panes
+                .first()
+                .expect("There must be an active pane")
+                .downgrade()
+        });
 
-    //         if let Member::Pane(center_pane) = &self.center.root {
-    //             if center_pane.read(cx).items_len() == 0 {
-    //                 return self.open_path(path, Some(pane), true, cx);
-    //             }
-    //         }
+        if let Member::Pane(center_pane) = &self.center.root {
+            if center_pane.read(cx).items_len() == 0 {
+                return self.open_path(path, Some(pane), true, cx);
+            }
+        }
 
-    //         let task = self.load_path(path.into(), cx);
-    //         cx.spawn(|this, mut cx| async move {
-    //             let (project_entry_id, build_item) = task.await?;
-    //             this.update(&mut cx, move |this, cx| -> Option<_> {
-    //                 let pane = pane.upgrade(cx)?;
-    //                 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
-    //                 new_pane.update(cx, |new_pane, cx| {
-    //                     Some(new_pane.open_item(project_entry_id, true, cx, build_item))
-    //                 })
-    //             })
-    //             .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
-    //         })
-    //     }
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|this, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            this.update(&mut cx, move |this, cx| -> Option<_> {
+                let pane = pane.upgrade()?;
+                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+                new_pane.update(cx, |new_pane, cx| {
+                    Some(new_pane.open_item(project_entry_id, true, cx, build_item))
+                })
+            })
+            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
+        })
+    }
 
     pub(crate) fn load_path(
         &mut self,
@@ -3029,10 +3030,10 @@ impl Workspace {
 
     fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
         self.panes.retain(|p| p != pane);
-        if true {
-            todo!()
-            // cx.focus(self.panes.last().unwrap());
-        }
+        self.panes
+            .last()
+            .unwrap()
+            .update(cx, |pane, cx| pane.focus(cx));
         if self.last_active_center_pane == Some(pane.downgrade()) {
             self.last_active_center_pane = None;
         }
@@ -3401,10 +3402,6 @@ impl Workspace {
         //     });
     }
 
-    // todo!()
-    //     #[cfg(any(test, feature = "test-support"))]
-    //     pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
-    //         use node_runtime::FakeNodeRuntime;
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
         use node_runtime::FakeNodeRuntime;
@@ -3423,7 +3420,9 @@ impl Workspace {
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             node_runtime: FakeNodeRuntime::new(),
         });
-        Self::new(0, project, app_state, cx)
+        let workspace = Self::new(0, project, app_state, cx);
+        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
+        workspace
     }
 
     //     fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
@@ -3476,6 +3475,10 @@ impl Workspace {
         div
     }
 
+    pub fn current_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
+        self.modal_layer.read(cx).current_modal()
+    }
+
     pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
     where
         B: FnOnce(&mut ViewContext<V>) -> V,
@@ -3699,6 +3702,7 @@ impl Render for Workspace {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let mut context = KeyContext::default();
         context.add("Workspace");
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
 
         self.add_workspace_actions_listeners(div())
             .key_context(context)
@@ -3706,7 +3710,7 @@ impl Render for Workspace {
             .size_full()
             .flex()
             .flex_col()
-            .font("Zed Sans")
+            .font(ui_font)
             .gap_0()
             .justify_start()
             .items_start()
@@ -3732,7 +3736,15 @@ impl Render for Workspace {
                             .flex_row()
                             .flex_1()
                             .h_full()
-                            .child(div().flex().flex_1().child(self.left_dock.clone()))
+                            // Left Dock
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(self.left_dock.clone()),
+                            )
+                            // Panes
                             .child(
                                 div()
                                     .flex()
@@ -3749,7 +3761,14 @@ impl Render for Workspace {
                                     ))
                                     .child(div().flex().flex_1().child(self.bottom_dock.clone())),
                             )
-                            .child(div().flex().flex_1().child(self.right_dock.clone())),
+                            // Right Dock
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(self.right_dock.clone()),
+                            ),
                     ),
             )
             .child(self.status_bar.clone())

crates/zed2/Cargo.toml 🔗

@@ -36,7 +36,7 @@ copilot = { package = "copilot2", path = "../copilot2" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
 # feedback = { path = "../feedback" }
-# file_finder = { path = "../file_finder" }
+file_finder = { package="file_finder2", path = "../file_finder2" }
 # search = { path = "../search" }
 fs = { package = "fs2", path = "../fs2" }
 fsevent = { path = "../fsevent" }

crates/zed2/src/main.rs 🔗

@@ -190,7 +190,7 @@ fn main() {
         // recent_projects::init(cx);
 
         go_to_line::init(cx);
-        // file_finder::init(cx);
+        file_finder::init(cx);
         // outline::init(cx);
         // project_symbols::init(cx);
         project_panel::init(Assets, cx);

crates/zed2/src/zed2.rs 🔗

@@ -502,7 +502,6 @@ fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext<Workspace>) {
         cx.update(|_, cx| {
             cx.quit();
         })?;
-
         anyhow::Ok(())
     })
     .detach_and_log_err(cx);