Merge branch 'main' into unborked-git-zed2-diagnostics-view

Julia created

Change summary

Cargo.lock                                     |    2 
crates/command_palette2/src/command_palette.rs |  239 -
crates/editor2/src/display_map.rs              |   13 
crates/editor2/src/display_map/block_map.rs    |    3 
crates/editor2/src/display_map/fold_map.rs     |  105 
crates/editor2/src/editor.rs                   |  491 ++--
crates/editor2/src/editor_tests.rs             |   68 
crates/editor2/src/element.rs                  |  783 +++---
crates/editor2/src/git.rs                      |    6 
crates/file_finder2/src/file_finder.rs         | 2321 +++++++++----------
crates/gpui2/docs/contexts.md                  |   41 
crates/gpui2/src/action.rs                     |   11 
crates/gpui2/src/app/test_context.rs           |   84 
crates/gpui2/src/element.rs                    |   46 
crates/gpui2/src/elements/div.rs               |   86 
crates/gpui2/src/elements/uniform_list.rs      |   17 
crates/gpui2/src/interactive.rs                |    1 
crates/gpui2/src/key_dispatch.rs               |   35 
crates/gpui2/src/platform.rs                   |    6 
crates/gpui2/src/platform/mac/text_system.rs   |   15 
crates/gpui2/src/platform/test/window.rs       |    6 
crates/gpui2/src/style.rs                      |    8 
crates/gpui2/src/text_system.rs                |   26 
crates/gpui2/src/view.rs                       |   71 
crates/gpui2/src/window.rs                     |   28 
crates/project/src/project.rs                  |    3 
crates/project/src/worktree.rs                 |    2 
crates/project2/src/project2.rs                |    3 
crates/project2/src/worktree.rs                |    2 
crates/workspace2/src/modal_layer.rs           |    2 
crates/workspace2/src/pane.rs                  |   13 
crates/workspace2/src/workspace2.rs            |   21 
crates/zed/Cargo.toml                          |    2 
33 files changed, 2,362 insertions(+), 2,198 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11316,7 +11316,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.113.0"
+version = "0.114.0"
 dependencies = [
  "activity_indicator",
  "ai",

crates/command_palette2/src/command_palette.rs 🔗

@@ -354,129 +354,116 @@ impl std::fmt::Debug for Command {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use std::sync::Arc;
-
-//     use super::*;
-//     use editor::Editor;
-//     use gpui::{executor::Deterministic, TestAppContext};
-//     use project::Project;
-//     use workspace::{AppState, Workspace};
-
-//     #[test]
-//     fn test_humanize_action_name() {
-//         assert_eq!(
-//             humanize_action_name("editor::GoToDefinition"),
-//             "editor: go to definition"
-//         );
-//         assert_eq!(
-//             humanize_action_name("editor::Backspace"),
-//             "editor: backspace"
-//         );
-//         assert_eq!(
-//             humanize_action_name("go_to_line::Deploy"),
-//             "go to line: deploy"
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-
-//         let project = Project::test(app_state.fs.clone(), [], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let editor = window.add_view(cx, |cx| {
-//             let mut editor = Editor::single_line(None, cx);
-//             editor.set_text("abc", cx);
-//             editor
-//         });
-
-//         workspace.update(cx, |workspace, cx| {
-//             cx.focus(&editor);
-//             workspace.add_item(Box::new(editor.clone()), cx)
-//         });
-
-//         workspace.update(cx, |workspace, cx| {
-//             toggle_command_palette(workspace, &Toggle, cx);
-//         });
-
-//         let palette = workspace.read_with(cx, |workspace, _| {
-//             workspace.modal::<CommandPalette>().unwrap()
-//         });
-
-//         palette
-//             .update(cx, |palette, cx| {
-//                 // Fill up palette's command list by running an empty query;
-//                 // we only need it to subsequently assert that the palette is initially
-//                 // sorted by command's name.
-//                 palette.delegate_mut().update_matches("".to_string(), cx)
-//             })
-//             .await;
-
-//         palette.update(cx, |palette, _| {
-//             let is_sorted =
-//                 |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
-//             assert!(is_sorted(&palette.delegate().actions));
-//         });
-
-//         palette
-//             .update(cx, |palette, cx| {
-//                 palette
-//                     .delegate_mut()
-//                     .update_matches("bcksp".to_string(), cx)
-//             })
-//             .await;
-
-//         palette.update(cx, |palette, cx| {
-//             assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
-//             palette.confirm(&Default::default(), cx);
-//         });
-//         deterministic.run_until_parked();
-//         editor.read_with(cx, |editor, cx| {
-//             assert_eq!(editor.text(cx), "ab");
-//         });
-
-//         // Add namespace filter, and redeploy the palette
-//         cx.update(|cx| {
-//             cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
-//                 filter.filtered_namespaces.insert("editor");
-//             })
-//         });
-
-//         workspace.update(cx, |workspace, cx| {
-//             toggle_command_palette(workspace, &Toggle, cx);
-//         });
-
-//         // Assert editor command not present
-//         let palette = workspace.read_with(cx, |workspace, _| {
-//             workspace.modal::<CommandPalette>().unwrap()
-//         });
-
-//         palette
-//             .update(cx, |palette, cx| {
-//                 palette
-//                     .delegate_mut()
-//                     .update_matches("bcksp".to_string(), cx)
-//             })
-//             .await;
-
-//         palette.update(cx, |palette, _| {
-//             assert!(palette.delegate().matches.is_empty())
-//         });
-//     }
-
-//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
-//         cx.update(|cx| {
-//             let app_state = AppState::test(cx);
-//             theme::init(cx);
-//             language::init(cx);
-//             editor::init(cx);
-//             workspace::init(app_state.clone(), cx);
-//             init(cx);
-//             Project::init_settings(cx);
-//             app_state
-//         })
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use std::sync::Arc;
+
+    use super::*;
+    use editor::Editor;
+    use gpui::TestAppContext;
+    use project::Project;
+    use workspace::{AppState, Workspace};
+
+    #[test]
+    fn test_humanize_action_name() {
+        assert_eq!(
+            humanize_action_name("editor::GoToDefinition"),
+            "editor: go to definition"
+        );
+        assert_eq!(
+            humanize_action_name("editor::Backspace"),
+            "editor: backspace"
+        );
+        assert_eq!(
+            humanize_action_name("go_to_line::Deploy"),
+            "go to line: deploy"
+        );
+    }
+
+    #[gpui::test]
+    async fn test_command_palette(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
+
+        let editor = cx.build_view(|cx| {
+            let mut editor = Editor::single_line(cx);
+            editor.set_text("abc", cx);
+            editor
+        });
+
+        workspace.update(cx, |workspace, cx| {
+            workspace.add_item(Box::new(editor.clone()), cx);
+            editor.update(cx, |editor, cx| editor.focus(cx))
+        });
+
+        cx.simulate_keystrokes("cmd-shift-p");
+
+        let palette = workspace.update(cx, |workspace, cx| {
+            workspace
+                .active_modal::<CommandPalette>(cx)
+                .unwrap()
+                .read(cx)
+                .picker
+                .clone()
+        });
+
+        palette.update(cx, |palette, _| {
+            assert!(palette.delegate.commands.len() > 5);
+            let is_sorted =
+                |actions: &[Command]| actions.windows(2).all(|pair| pair[0].name <= pair[1].name);
+            assert!(is_sorted(&palette.delegate.commands));
+        });
+
+        cx.simulate_input("bcksp");
+
+        palette.update(cx, |palette, _| {
+            assert_eq!(palette.delegate.matches[0].string, "editor: backspace");
+        });
+
+        cx.simulate_keystrokes("enter");
+
+        workspace.update(cx, |workspace, cx| {
+            assert!(workspace.active_modal::<CommandPalette>(cx).is_none());
+            assert_eq!(editor.read(cx).text(cx), "ab")
+        });
+
+        // Add namespace filter, and redeploy the palette
+        cx.update(|cx| {
+            cx.set_global(CommandPaletteFilter::default());
+            cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
+                filter.filtered_namespaces.insert("editor");
+            })
+        });
+
+        cx.simulate_keystrokes("cmd-shift-p");
+        cx.simulate_input("bcksp");
+
+        let palette = workspace.update(cx, |workspace, cx| {
+            workspace
+                .active_modal::<CommandPalette>(cx)
+                .unwrap()
+                .read(cx)
+                .picker
+                .clone()
+        });
+        palette.update(cx, |palette, _| {
+            assert!(palette.delegate.matches.is_empty())
+        });
+    }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init(cx);
+            language::init(cx);
+            editor::init(cx);
+            workspace::init(app_state.clone(), cx);
+            init(cx);
+            Project::init_settings(cx);
+            settings::load_default_keymap(cx);
+            app_state
+        })
+    }
+}

crates/editor2/src/display_map.rs 🔗

@@ -31,7 +31,7 @@ pub use block_map::{
     BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
-pub use self::fold_map::FoldPoint;
+pub use self::fold_map::{Fold, FoldPoint};
 pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -124,7 +124,7 @@ impl DisplayMap {
         self.fold(
             other
                 .folds_in_range(0..other.buffer_snapshot.len())
-                .map(|fold| fold.to_offset(&other.buffer_snapshot)),
+                .map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
             cx,
         );
     }
@@ -578,12 +578,7 @@ impl DisplaySnapshot {
             line.push_str(chunk.chunk);
 
             let text_style = if let Some(style) = chunk.style {
-                editor_style
-                    .text
-                    .clone()
-                    .highlight(style)
-                    .map(Cow::Owned)
-                    .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+                Cow::Owned(editor_style.text.clone().highlight(style))
             } else {
                 Cow::Borrowed(&editor_style.text)
             };
@@ -728,7 +723,7 @@ impl DisplaySnapshot {
         DisplayPoint(point)
     }
 
-    pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+    pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
     where
         T: ToOffset,
     {

crates/editor2/src/display_map/block_map.rs 🔗

@@ -2,7 +2,7 @@ use super::{
     wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
     Highlights,
 };
-use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
+use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
 use collections::{Bound, HashMap, HashSet};
 use gpui::{AnyElement, Pixels, ViewContext};
 use language::{BufferSnapshot, Chunk, Patch, Point};
@@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b> {
     pub em_width: Pixels,
     pub line_height: Pixels,
     pub block_id: usize,
+    pub editor_style: &'b EditorStyle,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]

crates/editor2/src/display_map/fold_map.rs 🔗

@@ -3,15 +3,16 @@ use super::{
     Highlights,
 };
 use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
-use gpui::{HighlightStyle, Hsla};
+use gpui::{ElementId, HighlightStyle, Hsla};
 use language::{Chunk, Edit, Point, TextSummary};
 use std::{
     any::TypeId,
     cmp::{self, Ordering},
     iter,
-    ops::{Add, AddAssign, Range, Sub},
+    ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
+use util::post_inc;
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct FoldPoint(pub Point);
@@ -90,12 +91,16 @@ impl<'a> FoldMapWriter<'a> {
             }
 
             // For now, ignore any ranges that span an excerpt boundary.
-            let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
-            if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
+            let fold_range =
+                FoldRange(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
+            if fold_range.0.start.excerpt_id != fold_range.0.end.excerpt_id {
                 continue;
             }
 
-            folds.push(fold);
+            folds.push(Fold {
+                id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
+                range: fold_range,
+            });
 
             let inlay_range =
                 snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
@@ -106,13 +111,13 @@ impl<'a> FoldMapWriter<'a> {
         }
 
         let buffer = &snapshot.buffer;
-        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
+        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer));
 
         self.0.snapshot.folds = {
             let mut new_tree = SumTree::new();
-            let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
+            let mut cursor = self.0.snapshot.folds.cursor::<FoldRange>();
             for fold in folds {
-                new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+                new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer);
                 new_tree.push(fold, buffer);
             }
             new_tree.append(cursor.suffix(buffer), buffer);
@@ -138,7 +143,8 @@ impl<'a> FoldMapWriter<'a> {
             let mut folds_cursor =
                 intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
             while let Some(fold) = folds_cursor.item() {
-                let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
+                let offset_range =
+                    fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
                 if offset_range.end > offset_range.start {
                     let inlay_range = snapshot.to_inlay_offset(offset_range.start)
                         ..snapshot.to_inlay_offset(offset_range.end);
@@ -175,6 +181,7 @@ impl<'a> FoldMapWriter<'a> {
 pub struct FoldMap {
     snapshot: FoldSnapshot,
     ellipses_color: Option<Hsla>,
+    next_fold_id: FoldId,
 }
 
 impl FoldMap {
@@ -197,6 +204,7 @@ impl FoldMap {
                 ellipses_color: None,
             },
             ellipses_color: None,
+            next_fold_id: FoldId::default(),
         };
         let snapshot = this.snapshot.clone();
         (this, snapshot)
@@ -242,8 +250,8 @@ impl FoldMap {
             while let Some(fold) = folds.next() {
                 if let Some(next_fold) = folds.peek() {
                     let comparison = fold
-                        .0
-                        .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
+                        .range
+                        .cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
                     assert!(comparison.is_le());
                 }
             }
@@ -304,9 +312,9 @@ impl FoldMap {
                 let anchor = inlay_snapshot
                     .buffer
                     .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
-                let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+                let mut folds_cursor = self.snapshot.folds.cursor::<FoldRange>();
                 folds_cursor.seek(
-                    &Fold(anchor..Anchor::max()),
+                    &FoldRange(anchor..Anchor::max()),
                     Bias::Left,
                     &inlay_snapshot.buffer,
                 );
@@ -315,8 +323,8 @@ impl FoldMap {
                     let inlay_snapshot = &inlay_snapshot;
                     move || {
                         let item = folds_cursor.item().map(|f| {
-                            let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
-                            let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+                            let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
+                            let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
                             inlay_snapshot.to_inlay_offset(buffer_start)
                                 ..inlay_snapshot.to_inlay_offset(buffer_end)
                         });
@@ -596,13 +604,13 @@ impl FoldSnapshot {
         self.transforms.summary().output.longest_row
     }
 
-    pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+    pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
     where
         T: ToOffset,
     {
         let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
         iter::from_fn(move || {
-            let item = folds.item().map(|f| &f.0);
+            let item = folds.item();
             folds.next(&self.inlay_snapshot.buffer);
             item
         })
@@ -830,10 +838,39 @@ impl sum_tree::Summary for TransformSummary {
     }
 }
 
-#[derive(Clone, Debug)]
-struct Fold(Range<Anchor>);
+#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
+pub struct FoldId(usize);
+
+impl Into<ElementId> for FoldId {
+    fn into(self) -> ElementId {
+        ElementId::Integer(self.0)
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Fold {
+    pub id: FoldId,
+    pub range: FoldRange,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct FoldRange(Range<Anchor>);
+
+impl Deref for FoldRange {
+    type Target = Range<Anchor>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for FoldRange {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
 
-impl Default for Fold {
+impl Default for FoldRange {
     fn default() -> Self {
         Self(Anchor::min()..Anchor::max())
     }
@@ -844,17 +881,17 @@ impl sum_tree::Item for Fold {
 
     fn summary(&self) -> Self::Summary {
         FoldSummary {
-            start: self.0.start.clone(),
-            end: self.0.end.clone(),
-            min_start: self.0.start.clone(),
-            max_end: self.0.end.clone(),
+            start: self.range.start.clone(),
+            end: self.range.end.clone(),
+            min_start: self.range.start.clone(),
+            max_end: self.range.end.clone(),
             count: 1,
         }
     }
 }
 
 #[derive(Clone, Debug)]
-struct FoldSummary {
+pub struct FoldSummary {
     start: Anchor,
     end: Anchor,
     min_start: Anchor,
@@ -900,14 +937,14 @@ impl sum_tree::Summary for FoldSummary {
     }
 }
 
-impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
+impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
     fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
         self.0.start = summary.start.clone();
         self.0.end = summary.end.clone();
     }
 }
 
-impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
+impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
     fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
         self.0.cmp(&other.0, buffer)
     }
@@ -1321,7 +1358,10 @@ mod tests {
         let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
         let fold_ranges = snapshot
             .folds_in_range(Point::new(1, 0)..Point::new(1, 3))
-            .map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
+            .map(|fold| {
+                fold.range.start.to_point(&buffer_snapshot)
+                    ..fold.range.end.to_point(&buffer_snapshot)
+            })
             .collect::<Vec<_>>();
         assert_eq!(
             fold_ranges,
@@ -1553,10 +1593,9 @@ mod tests {
                     .filter(|fold| {
                         let start = buffer_snapshot.anchor_before(start);
                         let end = buffer_snapshot.anchor_after(end);
-                        start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
-                            && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
+                        start.cmp(&fold.range.end, &buffer_snapshot) == Ordering::Less
+                            && end.cmp(&fold.range.start, &buffer_snapshot) == Ordering::Greater
                     })
-                    .map(|fold| fold.0)
                     .collect::<Vec<_>>();
 
                 assert_eq!(
@@ -1639,10 +1678,10 @@ mod tests {
             let buffer = &inlay_snapshot.buffer;
             let mut folds = self.snapshot.folds.items(buffer);
             // Ensure sorting doesn't change how folds get merged and displayed.
-            folds.sort_by(|a, b| a.0.cmp(&b.0, buffer));
+            folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
             let mut fold_ranges = folds
                 .iter()
-                .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer))
+                .map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
                 .peekable();
 
             let mut merged_ranges = Vec::new();

crates/editor2/src/editor.rs 🔗

@@ -100,7 +100,9 @@ use theme::{
 use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
-    item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace,
+    item::{ItemEvent, ItemHandle},
+    searchable::SearchEvent,
+    ItemNavHistory, SplitDirection, ViewId, Workspace,
 };
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -1878,10 +1880,8 @@ impl Editor {
         );
 
         let focus_handle = cx.focus_handle();
-        cx.on_focus_in(&focus_handle, Self::handle_focus_in)
-            .detach();
-        cx.on_focus_out(&focus_handle, Self::handle_focus_out)
-            .detach();
+        cx.on_focus(&focus_handle, Self::handle_focus).detach();
+        cx.on_blur(&focus_handle, Self::handle_blur).detach();
 
         let mut this = Self {
             handle: cx.view().downgrade(),
@@ -4372,69 +4372,42 @@ impl Editor {
         }
     }
 
-    //     pub fn render_fold_indicators(
-    //         &self,
-    //         fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
-    //         style: &EditorStyle,
-    //         gutter_hovered: bool,
-    //         line_height: f32,
-    //         gutter_margin: f32,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Vec<Option<AnyElement<Self>>> {
-    //         enum FoldIndicators {}
-
-    //         let style = style.folds.clone();
-
-    //         fold_data
-    //             .iter()
-    //             .enumerate()
-    //             .map(|(ix, fold_data)| {
-    //                 fold_data
-    //                     .map(|(fold_status, buffer_row, active)| {
-    //                         (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
-    //                             MouseEventHandler::new::<FoldIndicators, _>(
-    //                                 ix as usize,
-    //                                 cx,
-    //                                 |mouse_state, _| {
-    //                                     Svg::new(match fold_status {
-    //                                         FoldStatus::Folded => style.folded_icon.clone(),
-    //                                         FoldStatus::Foldable => style.foldable_icon.clone(),
-    //                                     })
-    //                                     .with_color(
-    //                                         style
-    //                                             .indicator
-    //                                             .in_state(fold_status == FoldStatus::Folded)
-    //                                             .style_for(mouse_state)
-    //                                             .color,
-    //                                     )
-    //                                     .constrained()
-    //                                     .with_width(gutter_margin * style.icon_margin_scale)
-    //                                     .aligned()
-    //                                     .constrained()
-    //                                     .with_height(line_height)
-    //                                     .with_width(gutter_margin)
-    //                                     .aligned()
-    //                                 },
-    //                             )
-    //                             .with_cursor_style(CursorStyle::PointingHand)
-    //                             .with_padding(Padding::uniform(3.))
-    //                             .on_click(MouseButton::Left, {
-    //                                 move |_, editor, cx| match fold_status {
-    //                                     FoldStatus::Folded => {
-    //                                         editor.unfold_at(&UnfoldAt { buffer_row }, cx);
-    //                                     }
-    //                                     FoldStatus::Foldable => {
-    //                                         editor.fold_at(&FoldAt { buffer_row }, cx);
-    //                                     }
-    //                                 }
-    //                             })
-    //                             .into_any()
-    //                         })
-    //                     })
-    //                     .flatten()
-    //             })
-    //             .collect()
-    //     }
+    pub fn render_fold_indicators(
+        &self,
+        fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
+        style: &EditorStyle,
+        gutter_hovered: bool,
+        line_height: Pixels,
+        gutter_margin: Pixels,
+        cx: &mut ViewContext<Self>,
+    ) -> Vec<Option<AnyElement<Self>>> {
+        fold_data
+            .iter()
+            .enumerate()
+            .map(|(ix, fold_data)| {
+                fold_data
+                    .map(|(fold_status, buffer_row, active)| {
+                        (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
+                            let icon = match fold_status {
+                                FoldStatus::Folded => ui::Icon::ChevronRight,
+                                FoldStatus::Foldable => ui::Icon::ChevronDown,
+                            };
+                            IconButton::new(ix as usize, icon)
+                                .on_click(move |editor: &mut Editor, cx| match fold_status {
+                                    FoldStatus::Folded => {
+                                        editor.unfold_at(&UnfoldAt { buffer_row }, cx);
+                                    }
+                                    FoldStatus::Foldable => {
+                                        editor.fold_at(&FoldAt { buffer_row }, cx);
+                                    }
+                                })
+                                .render()
+                        })
+                    })
+                    .flatten()
+            })
+            .collect()
+    }
 
     pub fn context_menu_visible(&self) -> bool {
         self.context_menu
@@ -5330,8 +5303,8 @@ impl Editor {
                         buffer.anchor_before(range_to_move.start)
                             ..buffer.anchor_after(range_to_move.end),
                     ) {
-                        let mut start = fold.start.to_point(&buffer);
-                        let mut end = fold.end.to_point(&buffer);
+                        let mut start = fold.range.start.to_point(&buffer);
+                        let mut end = fold.range.end.to_point(&buffer);
                         start.row -= row_delta;
                         end.row -= row_delta;
                         refold_ranges.push(start..end);
@@ -5421,8 +5394,8 @@ impl Editor {
                         buffer.anchor_before(range_to_move.start)
                             ..buffer.anchor_after(range_to_move.end),
                     ) {
-                        let mut start = fold.start.to_point(&buffer);
-                        let mut end = fold.end.to_point(&buffer);
+                        let mut start = fold.range.start.to_point(&buffer);
+                        let mut end = fold.range.end.to_point(&buffer);
                         start.row += row_delta;
                         end.row += row_delta;
                         refold_ranges.push(start..end);
@@ -7690,183 +7663,203 @@ impl Editor {
         }
     }
 
-    // pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-    //     use language::ToOffset as _;
+    pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+        use language::ToOffset as _;
 
-    //     let project = self.project.clone()?;
-    //     let selection = self.selections.newest_anchor().clone();
-    //     let (cursor_buffer, cursor_buffer_position) = self
-    //         .buffer
-    //         .read(cx)
-    //         .text_anchor_for_position(selection.head(), cx)?;
-    //     let (tail_buffer, _) = self
-    //         .buffer
-    //         .read(cx)
-    //         .text_anchor_for_position(selection.tail(), cx)?;
-    //     if tail_buffer != cursor_buffer {
-    //         return None;
-    //     }
+        let project = self.project.clone()?;
+        let selection = self.selections.newest_anchor().clone();
+        let (cursor_buffer, cursor_buffer_position) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.head(), cx)?;
+        let (tail_buffer, _) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(selection.tail(), cx)?;
+        if tail_buffer != cursor_buffer {
+            return None;
+        }
 
-    //     let snapshot = cursor_buffer.read(cx).snapshot();
-    //     let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
-    //     let prepare_rename = project.update(cx, |project, cx| {
-    //         project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
-    //     });
+        let snapshot = cursor_buffer.read(cx).snapshot();
+        let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
+        let prepare_rename = project.update(cx, |project, cx| {
+            project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
+        });
 
-    //     Some(cx.spawn(|this, mut cx| async move {
-    //         let rename_range = if let Some(range) = prepare_rename.await? {
-    //             Some(range)
-    //         } else {
-    //             this.update(&mut cx, |this, cx| {
-    //                 let buffer = this.buffer.read(cx).snapshot(cx);
-    //                 let mut buffer_highlights = this
-    //                     .document_highlights_for_position(selection.head(), &buffer)
-    //                     .filter(|highlight| {
-    //                         highlight.start.excerpt_id == selection.head().excerpt_id
-    //                             && highlight.end.excerpt_id == selection.head().excerpt_id
-    //                     });
-    //                 buffer_highlights
-    //                     .next()
-    //                     .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
-    //             })?
-    //         };
-    //         if let Some(rename_range) = rename_range {
-    //             let rename_buffer_range = rename_range.to_offset(&snapshot);
-    //             let cursor_offset_in_rename_range =
-    //                 cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
-
-    //             this.update(&mut cx, |this, cx| {
-    //                 this.take_rename(false, cx);
-    //                 let buffer = this.buffer.read(cx).read(cx);
-    //                 let cursor_offset = selection.head().to_offset(&buffer);
-    //                 let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
-    //                 let rename_end = rename_start + rename_buffer_range.len();
-    //                 let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
-    //                 let mut old_highlight_id = None;
-    //                 let old_name: Arc<str> = buffer
-    //                     .chunks(rename_start..rename_end, true)
-    //                     .map(|chunk| {
-    //                         if old_highlight_id.is_none() {
-    //                             old_highlight_id = chunk.syntax_highlight_id;
-    //                         }
-    //                         chunk.text
-    //                     })
-    //                     .collect::<String>()
-    //                     .into();
-
-    //                 drop(buffer);
-
-    //                 // Position the selection in the rename editor so that it matches the current selection.
-    //                 this.show_local_selections = false;
-    //                 let rename_editor = cx.build_view(|cx| {
-    //                     let mut editor = Editor::single_line(cx);
-    //                     if let Some(old_highlight_id) = old_highlight_id {
-    //                         editor.override_text_style =
-    //                             Some(Box::new(move |style| old_highlight_id.style(&style.syntax)));
-    //                     }
-    //                     editor.buffer.update(cx, |buffer, cx| {
-    //                         buffer.edit([(0..0, old_name.clone())], None, cx)
-    //                     });
-    //                     editor.select_all(&SelectAll, cx);
-    //                     editor
-    //                 });
+        Some(cx.spawn(|this, mut cx| async move {
+            let rename_range = if let Some(range) = prepare_rename.await? {
+                Some(range)
+            } else {
+                this.update(&mut cx, |this, cx| {
+                    let buffer = this.buffer.read(cx).snapshot(cx);
+                    let mut buffer_highlights = this
+                        .document_highlights_for_position(selection.head(), &buffer)
+                        .filter(|highlight| {
+                            highlight.start.excerpt_id == selection.head().excerpt_id
+                                && highlight.end.excerpt_id == selection.head().excerpt_id
+                        });
+                    buffer_highlights
+                        .next()
+                        .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
+                })?
+            };
+            if let Some(rename_range) = rename_range {
+                let rename_buffer_range = rename_range.to_offset(&snapshot);
+                let cursor_offset_in_rename_range =
+                    cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
 
-    //                 let ranges = this
-    //                     .clear_background_highlights::<DocumentHighlightWrite>(cx)
-    //                     .into_iter()
-    //                     .flat_map(|(_, ranges)| ranges.into_iter())
-    //                     .chain(
-    //                         this.clear_background_highlights::<DocumentHighlightRead>(cx)
-    //                             .into_iter()
-    //                             .flat_map(|(_, ranges)| ranges.into_iter()),
-    //                     )
-    //                     .collect();
-
-    //                 this.highlight_text::<Rename>(
-    //                     ranges,
-    //                     HighlightStyle {
-    //                         fade_out: Some(style.rename_fade),
-    //                         ..Default::default()
-    //                     },
-    //                     cx,
-    //                 );
-    //                 cx.focus(&rename_editor);
-    //                 let block_id = this.insert_blocks(
-    //                     [BlockProperties {
-    //                         style: BlockStyle::Flex,
-    //                         position: range.start.clone(),
-    //                         height: 1,
-    //                         render: Arc::new({
-    //                             let editor = rename_editor.clone();
-    //                             move |cx: &mut BlockContext| {
-    //                                 ChildView::new(&editor, cx)
-    //                                     .contained()
-    //                                     .with_padding_left(cx.anchor_x)
-    //                                     .into_any()
-    //                             }
-    //                         }),
-    //                         disposition: BlockDisposition::Below,
-    //                     }],
-    //                     Some(Autoscroll::fit()),
-    //                     cx,
-    //                 )[0];
-    //                 this.pending_rename = Some(RenameState {
-    //                     range,
-    //                     old_name,
-    //                     editor: rename_editor,
-    //                     block_id,
-    //                 });
-    //             })?;
-    //         }
+                this.update(&mut cx, |this, cx| {
+                    this.take_rename(false, cx);
+                    let buffer = this.buffer.read(cx).read(cx);
+                    let cursor_offset = selection.head().to_offset(&buffer);
+                    let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
+                    let rename_end = rename_start + rename_buffer_range.len();
+                    let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
+                    let mut old_highlight_id = None;
+                    let old_name: Arc<str> = buffer
+                        .chunks(rename_start..rename_end, true)
+                        .map(|chunk| {
+                            if old_highlight_id.is_none() {
+                                old_highlight_id = chunk.syntax_highlight_id;
+                            }
+                            chunk.text
+                        })
+                        .collect::<String>()
+                        .into();
 
-    //         Ok(())
-    //     }))
-    // }
+                    drop(buffer);
 
-    //     pub fn confirm_rename(
-    //         workspace: &mut Workspace,
-    //         _: &ConfirmRename,
-    //         cx: &mut ViewContext<Workspace>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
-
-    //         let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
-    //             let rename = editor.take_rename(false, cx)?;
-    //             let buffer = editor.buffer.read(cx);
-    //             let (start_buffer, start) =
-    //                 buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
-    //             let (end_buffer, end) =
-    //                 buffer.text_anchor_for_position(rename.range.end.clone(), cx)?;
-    //             if start_buffer == end_buffer {
-    //                 let new_name = rename.editor.read(cx).text(cx);
-    //                 Some((start_buffer, start..end, rename.old_name, new_name))
-    //             } else {
-    //                 None
-    //             }
-    //         })?;
+                    // Position the selection in the rename editor so that it matches the current selection.
+                    this.show_local_selections = false;
+                    let rename_editor = cx.build_view(|cx| {
+                        let mut editor = Editor::single_line(cx);
+                        editor.buffer.update(cx, |buffer, cx| {
+                            buffer.edit([(0..0, old_name.clone())], None, cx)
+                        });
+                        editor.select_all(&SelectAll, cx);
+                        editor
+                    });
 
-    //         let rename = workspace.project().clone().update(cx, |project, cx| {
-    //             project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
-    //         });
+                    let ranges = this
+                        .clear_background_highlights::<DocumentHighlightWrite>(cx)
+                        .into_iter()
+                        .flat_map(|(_, ranges)| ranges.into_iter())
+                        .chain(
+                            this.clear_background_highlights::<DocumentHighlightRead>(cx)
+                                .into_iter()
+                                .flat_map(|(_, ranges)| ranges.into_iter()),
+                        )
+                        .collect();
 
-    //         let editor = editor.downgrade();
-    //         Some(cx.spawn(|workspace, mut cx| async move {
-    //             let project_transaction = rename.await?;
-    //             Self::open_project_transaction(
-    //                 &editor,
-    //                 workspace,
-    //                 project_transaction,
-    //                 format!("Rename: {} → {}", old_name, new_name),
-    //                 cx.clone(),
-    //             )
-    //             .await?;
+                    this.highlight_text::<Rename>(
+                        ranges,
+                        HighlightStyle {
+                            fade_out: Some(0.6),
+                            ..Default::default()
+                        },
+                        cx,
+                    );
+                    let rename_focus_handle = rename_editor.focus_handle(cx);
+                    cx.focus(&rename_focus_handle);
+                    let block_id = this.insert_blocks(
+                        [BlockProperties {
+                            style: BlockStyle::Flex,
+                            position: range.start.clone(),
+                            height: 1,
+                            render: Arc::new({
+                                let rename_editor = rename_editor.clone();
+                                move |cx: &mut BlockContext| {
+                                    let mut text_style = cx.editor_style.text.clone();
+                                    if let Some(highlight_style) = old_highlight_id
+                                        .and_then(|h| h.style(&cx.editor_style.syntax))
+                                    {
+                                        text_style = text_style.highlight(highlight_style);
+                                    }
+                                    div()
+                                        .pl(cx.anchor_x)
+                                        .child(rename_editor.render_with(EditorElement::new(
+                                            &rename_editor,
+                                            EditorStyle {
+                                                background: cx.theme().system().transparent,
+                                                local_player: cx.editor_style.local_player,
+                                                text: text_style,
+                                                scrollbar_width: cx.editor_style.scrollbar_width,
+                                                syntax: cx.editor_style.syntax.clone(),
+                                                diagnostic_style:
+                                                    cx.editor_style.diagnostic_style.clone(),
+                                            },
+                                        )))
+                                        .render()
+                                }
+                            }),
+                            disposition: BlockDisposition::Below,
+                        }],
+                        Some(Autoscroll::fit()),
+                        cx,
+                    )[0];
+                    this.pending_rename = Some(RenameState {
+                        range,
+                        old_name,
+                        editor: rename_editor,
+                        block_id,
+                    });
+                })?;
+            }
 
-    //             editor.update(&mut cx, |editor, cx| {
-    //                 editor.refresh_document_highlights(cx);
-    //             })?;
-    //             Ok(())
-    //         }))
-    //     }
+            Ok(())
+        }))
+    }
+
+    pub fn confirm_rename(
+        &mut self,
+        _: &ConfirmRename,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let rename = self.take_rename(false, cx)?;
+        let workspace = self.workspace()?;
+        let (start_buffer, start) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(rename.range.start.clone(), cx)?;
+        let (end_buffer, end) = self
+            .buffer
+            .read(cx)
+            .text_anchor_for_position(rename.range.end.clone(), cx)?;
+        if start_buffer != end_buffer {
+            return None;
+        }
+
+        let buffer = start_buffer;
+        let range = start..end;
+        let old_name = rename.old_name;
+        let new_name = rename.editor.read(cx).text(cx);
+
+        let rename = workspace
+            .read(cx)
+            .project()
+            .clone()
+            .update(cx, |project, cx| {
+                project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
+            });
+        let workspace = workspace.downgrade();
+
+        Some(cx.spawn(|editor, mut cx| async move {
+            let project_transaction = rename.await?;
+            Self::open_project_transaction(
+                &editor,
+                workspace,
+                project_transaction,
+                format!("Rename: {} → {}", old_name, new_name),
+                cx.clone(),
+            )
+            .await?;
+
+            editor.update(&mut cx, |editor, cx| {
+                editor.refresh_document_highlights(cx);
+            })?;
+            Ok(())
+        }))
+    }
 
     fn take_rename(
         &mut self,
@@ -7874,6 +7867,10 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<RenameState> {
         let rename = self.pending_rename.take()?;
+        if rename.editor.focus_handle(cx).is_focused(cx) {
+            cx.focus(&self.focus_handle);
+        }
+
         self.remove_blocks(
             [rename.block_id].into_iter().collect(),
             Some(Autoscroll::fit()),
@@ -9172,17 +9169,13 @@ impl Editor {
         self.focus_handle.is_focused(cx)
     }
 
-    fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
-        if self.focus_handle.is_focused(cx) {
-            // todo!()
-            // let focused_event = EditorFocused(cx.handle());
-            // cx.emit_global(focused_event);
-            cx.emit(EditorEvent::Focused);
-        }
+    fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.emit(EditorEvent::Focused);
+
         if let Some(rename) = self.pending_rename.as_ref() {
             let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
             cx.focus(&rename_editor_focus_handle);
-        } else if self.focus_handle.is_focused(cx) {
+        } else {
             self.blink_manager.update(cx, BlinkManager::enable);
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
@@ -9198,7 +9191,7 @@ impl Editor {
         }
     }
 
-    fn handle_focus_out(&mut self, cx: &mut ViewContext<Self>) {
+    fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
         // todo!()
         // let blurred_event = EditorBlurred(cx.handle());
         // cx.emit_global(blurred_event);

crates/editor2/src/editor_tests.rs 🔗

@@ -3851,12 +3851,12 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
+    let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
 
     view.condition::<crate::EditorEvent>(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.change_selections(None, cx, |s| {
             s.select_display_ranges([
                 DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
@@ -3867,7 +3867,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| { view.selections.display_ranges(cx) }),
+        view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
         &[
             DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
             DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@@ -3875,50 +3875,50 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
         ]
     );
 
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[
             DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
             DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
         ]
     );
 
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
     );
 
     // Trying to expand the selected syntax node one more time has no effect.
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
     );
 
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[
             DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
             DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
         ]
     );
 
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[
             DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
             DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@@ -3926,11 +3926,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
         ]
     );
 
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[
             DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
             DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
@@ -3939,11 +3939,11 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
     );
 
     // Trying to shrink the selected syntax node one more time has no effect.
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[
             DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
             DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
@@ -3953,7 +3953,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
 
     // Ensure that we keep expanding the selection if the larger selection starts or ends within
     // a fold.
-    view.update(&mut cx, |view, cx| {
+    view.update(cx, |view, cx| {
         view.fold_ranges(
             vec![
                 Point::new(0, 21)..Point::new(0, 24),
@@ -3965,7 +3965,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
         view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
     });
     assert_eq!(
-        view.update(&mut cx, |view, cx| view.selections.display_ranges(cx)),
+        view.update(cx, |view, cx| view.selections.display_ranges(cx)),
         &[
             DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
             DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
@@ -4017,8 +4017,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     editor
         .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
         .await;
@@ -4583,8 +4582,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
@@ -4734,8 +4732,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     editor
         .condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
@@ -4957,8 +4954,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
     assert!(cx.read(|cx| editor.is_dirty(cx)));
 
@@ -5077,8 +5073,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
     assert!(cx.read(|cx| editor.is_dirty(cx)));
 
@@ -5205,8 +5200,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
 
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
 
     let format = editor
@@ -5993,8 +5987,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
-    let cx = &mut cx;
+    let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
     view.update(cx, |view, cx| {
         assert_eq!(view.text(cx), "aaaa\nbbbb");
         view.change_selections(None, cx, |s| {
@@ -6064,8 +6057,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
         multibuffer
     });
 
-    let (view, mut cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
-    let cx = &mut cx;
+    let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx));
     view.update(cx, |view, cx| {
         let (expected_text, selection_ranges) = marked_text_ranges(
             indoc! {"
@@ -6302,8 +6294,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
         Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
     });
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (view, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     view.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
         .await;
 
@@ -8112,8 +8103,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
 
     let buffer_text = "one\ntwo\nthree\n";
     let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
-    let (editor, mut cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
-    let cx = &mut cx;
+    let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
     editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
 
     editor

crates/editor2/src/element.rs 🔗

@@ -18,11 +18,12 @@ use crate::{
 use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use gpui::{
-    point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow,
-    Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
-    ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line, MouseButton,
-    MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size,
-    Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext,
+    div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
+    BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
+    ElementId, ElementInputHandler, Entity, EntityId, Hsla,
+    InteractiveComponent, Line, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
+    ParentComponent, Pixels, ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled,
+    TextRun, TextStyle, View, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -487,23 +488,26 @@ impl EditorElement {
             }
         }
 
-        // todo!("fold indicators")
-        // for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
-        //     if let Some(indicator) = fold_indicator.as_mut() {
-        //         let position = point(
-        //             bounds.width() - layout.gutter_padding,
-        //             ix as f32 * line_height - (scroll_top % line_height),
-        //         );
-        //         let centering_offset = point(
-        //             (layout.gutter_padding + layout.gutter_margin - indicator.size().x) / 2.,
-        //             (line_height - indicator.size().y) / 2.,
-        //         );
-
-        //         let indicator_origin = bounds.origin + position + centering_offset;
+        for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
+            if let Some(fold_indicator) = fold_indicator.as_mut() {
+                let available_space = size(
+                    AvailableSpace::MinContent,
+                    AvailableSpace::Definite(line_height * 0.55),
+                );
+                let fold_indicator_size = fold_indicator.measure(available_space, editor, cx);
 
-        //         indicator.paint(indicator_origin, visible_bounds, editor, cx);
-        //     }
-        // }
+                let position = point(
+                    bounds.size.width - layout.gutter_padding,
+                    ix as f32 * line_height - (scroll_top % line_height),
+                );
+                let centering_offset = point(
+                    (layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) / 2.,
+                    (line_height - fold_indicator_size.height) / 2.,
+                );
+                let origin = bounds.origin + position + centering_offset;
+                fold_indicator.draw(origin, available_space, editor, cx);
+            }
+        }
 
         if let Some(indicator) = layout.code_actions_indicator.as_mut() {
             let available_space = size(
@@ -612,311 +616,341 @@ impl EditorElement {
 
     fn paint_text(
         &mut self,
-        bounds: Bounds<Pixels>,
+        text_bounds: Bounds<Pixels>,
         layout: &mut LayoutState,
         editor: &mut Editor,
         cx: &mut ViewContext<Editor>,
     ) {
         let scroll_position = layout.position_map.snapshot.scroll_position();
         let start_row = layout.visible_display_row_range.start;
-        let scroll_top = scroll_position.y * layout.position_map.line_height;
-        let max_glyph_width = layout.position_map.em_width;
-        let scroll_left = scroll_position.x * max_glyph_width;
-        let content_origin = bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
+        let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
         let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
 
-        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-            // todo!("cursor region")
-            // cx.scene().push_cursor_region(CursorRegion {
-            //     bounds,
-            //     style: if !editor.link_go_to_definition_state.definitions.is_empty {
-            //         CursorStyle::PointingHand
-            //     } else {
-            //         CursorStyle::IBeam
-            //     },
-            // });
-
-            // todo!("fold ranges")
-            // let fold_corner_radius =
-            //     self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
-            // for (id, range, color) in layout.fold_ranges.iter() {
-            //     self.paint_highlighted_range(
-            //         range.clone(),
-            //         *color,
-            //         fold_corner_radius,
-            //         fold_corner_radius * 2.,
-            //         layout,
-            //         content_origin,
-            //         scroll_top,
-            //         scroll_left,
-            //         bounds,
-            //         cx,
-            //     );
-
-            //     for bound in range_to_bounds(
-            //         &range,
-            //         content_origin,
-            //         scroll_left,
-            //         scroll_top,
-            //         &layout.visible_display_row_range,
-            //         line_end_overshoot,
-            //         &layout.position_map,
-            //     ) {
-            //         cx.scene().push_cursor_region(CursorRegion {
-            //             bounds: bound,
-            //             style: CursorStyle::PointingHand,
-            //         });
-
-            //         let display_row = range.start.row();
-
-            //         let buffer_row = DisplayPoint::new(display_row, 0)
-            //             .to_point(&layout.position_map.snapshot.display_snapshot)
-            //             .row;
-
-            //         let view_id = cx.view_id();
-            //         cx.scene().push_mouse_region(
-            //             MouseRegion::new::<FoldMarkers>(view_id, *id as usize, bound)
-            //                 .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
-            //                     editor.unfold_at(&UnfoldAt { buffer_row }, cx)
-            //                 })
-            //                 .with_notify_on_hover(true)
-            //                 .with_notify_on_click(true),
-            //         )
-            //     }
-            // }
-
-            for (range, color) in &layout.highlighted_ranges {
-                self.paint_highlighted_range(
-                    range.clone(),
-                    *color,
-                    Pixels::ZERO,
-                    line_end_overshoot,
-                    layout,
-                    content_origin,
-                    scroll_top,
-                    scroll_left,
-                    bounds,
-                    cx,
-                );
-            }
+        cx.with_content_mask(
+            Some(ContentMask {
+                bounds: text_bounds,
+            }),
+            |cx| {
+                // todo!("cursor region")
+                // cx.scene().push_cursor_region(CursorRegion {
+                //     bounds,
+                //     style: if !editor.link_go_to_definition_state.definitions.is_empty {
+                //         CursorStyle::PointingHand
+                //     } else {
+                //         CursorStyle::IBeam
+                //     },
+                // });
+
+                let fold_corner_radius = 0.15 * layout.position_map.line_height;
+                cx.with_element_id(Some("folds"), |cx| {
+                    let snapshot = &layout.position_map.snapshot;
+                    for fold in snapshot.folds_in_range(layout.visible_anchor_range.clone()) {
+                        let fold_range = fold.range.clone();
+                        let display_range = fold.range.start.to_display_point(&snapshot)
+                            ..fold.range.end.to_display_point(&snapshot);
+                        debug_assert_eq!(display_range.start.row(), display_range.end.row());
+                        let row = display_range.start.row();
+
+                        let line_layout = &layout.position_map.line_layouts
+                            [(row - layout.visible_display_row_range.start) as usize]
+                            .line;
+                        let start_x = content_origin.x
+                            + line_layout.x_for_index(display_range.start.column() as usize)
+                            - layout.position_map.scroll_position.x;
+                        let start_y = content_origin.y
+                            + row as f32 * layout.position_map.line_height
+                            - layout.position_map.scroll_position.y;
+                        let end_x = content_origin.x
+                            + line_layout.x_for_index(display_range.end.column() as usize)
+                            - layout.position_map.scroll_position.x;
+
+                        let fold_bounds = Bounds {
+                            origin: point(start_x, start_y),
+                            size: size(end_x - start_x, layout.position_map.line_height),
+                        };
 
-            let mut cursors = SmallVec::<[Cursor; 32]>::new();
-            let corner_radius = 0.15 * layout.position_map.line_height;
-            let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
+                        let fold_background = cx.with_z_index(1, |cx| {
+                            div()
+                                .id(fold.id)
+                                .size_full()
+                                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+                                .on_click(move |editor: &mut Editor, _, cx| {
+                                    editor.unfold_ranges(
+                                        [fold_range.start..fold_range.end],
+                                        true,
+                                        false,
+                                        cx,
+                                    );
+                                    cx.stop_propagation();
+                                })
+                                .draw(
+                                    fold_bounds.origin,
+                                    fold_bounds.size,
+                                    editor,
+                                    cx,
+                                    |fold_element_state, cx| {
+                                        if fold_element_state.is_active() {
+                                            gpui::blue()
+                                        } else if fold_bounds.contains_point(&cx.mouse_position()) {
+                                            gpui::black()
+                                        } else {
+                                            gpui::red()
+                                        }
+                                    },
+                                )
+                        });
+
+                        self.paint_highlighted_range(
+                            display_range.clone(),
+                            fold_background,
+                            fold_corner_radius,
+                            fold_corner_radius * 2.,
+                            layout,
+                            content_origin,
+                            text_bounds,
+                            cx,
+                        );
+                    }
+                });
 
-            for (selection_style, selections) in &layout.selections {
-                for selection in selections {
+                for (range, color) in &layout.highlighted_ranges {
                     self.paint_highlighted_range(
-                        selection.range.clone(),
-                        selection_style.selection,
-                        corner_radius,
-                        corner_radius * 2.,
+                        range.clone(),
+                        *color,
+                        Pixels::ZERO,
+                        line_end_overshoot,
                         layout,
                         content_origin,
-                        scroll_top,
-                        scroll_left,
-                        bounds,
+                        text_bounds,
                         cx,
                     );
+                }
 
-                    if selection.is_local && !selection.range.is_empty() {
-                        invisible_display_ranges.push(selection.range.clone());
-                    }
+                let mut cursors = SmallVec::<[Cursor; 32]>::new();
+                let corner_radius = 0.15 * layout.position_map.line_height;
+                let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
+
+                for (selection_style, selections) in &layout.selections {
+                    for selection in selections {
+                        self.paint_highlighted_range(
+                            selection.range.clone(),
+                            selection_style.selection,
+                            corner_radius,
+                            corner_radius * 2.,
+                            layout,
+                            content_origin,
+                            text_bounds,
+                            cx,
+                        );
 
-                    if !selection.is_local || editor.show_local_cursors(cx) {
-                        let cursor_position = selection.head;
-                        if layout
-                            .visible_display_row_range
-                            .contains(&cursor_position.row())
-                        {
-                            let cursor_row_layout = &layout.position_map.line_layouts
-                                [(cursor_position.row() - start_row) as usize]
-                                .line;
-                            let cursor_column = cursor_position.column() as usize;
-
-                            let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
-                            let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1)
-                                - cursor_character_x;
-                            if block_width == Pixels::ZERO {
-                                block_width = layout.position_map.em_width;
-                            }
-                            let block_text = if let CursorShape::Block = selection.cursor_shape {
-                                layout
-                                    .position_map
-                                    .snapshot
-                                    .chars_at(cursor_position)
-                                    .next()
-                                    .and_then(|(character, _)| {
-                                        let text = character.to_string();
-                                        cx.text_system()
-                                            .layout_text(
-                                                &text,
-                                                cursor_row_layout.font_size,
-                                                &[TextRun {
-                                                    len: text.len(),
-                                                    font: self.style.text.font(),
-                                                    color: self.style.background,
-                                                    underline: None,
-                                                }],
-                                                None,
-                                            )
-                                            .unwrap()
-                                            .pop()
-                                    })
-                            } else {
-                                None
-                            };
-
-                            let x = cursor_character_x - scroll_left;
-                            let y = cursor_position.row() as f32 * layout.position_map.line_height
-                                - scroll_top;
-                            if selection.is_newest {
-                                editor.pixel_position_of_newest_cursor = Some(point(
-                                    bounds.origin.x + x + block_width / 2.,
-                                    bounds.origin.y + y + layout.position_map.line_height / 2.,
-                                ));
+                        if selection.is_local && !selection.range.is_empty() {
+                            invisible_display_ranges.push(selection.range.clone());
+                        }
+
+                        if !selection.is_local || editor.show_local_cursors(cx) {
+                            let cursor_position = selection.head;
+                            if layout
+                                .visible_display_row_range
+                                .contains(&cursor_position.row())
+                            {
+                                let cursor_row_layout = &layout.position_map.line_layouts
+                                    [(cursor_position.row() - start_row) as usize]
+                                    .line;
+                                let cursor_column = cursor_position.column() as usize;
+
+                                let cursor_character_x =
+                                    cursor_row_layout.x_for_index(cursor_column);
+                                let mut block_width = cursor_row_layout
+                                    .x_for_index(cursor_column + 1)
+                                    - cursor_character_x;
+                                if block_width == Pixels::ZERO {
+                                    block_width = layout.position_map.em_width;
+                                }
+                                let block_text = if let CursorShape::Block = selection.cursor_shape
+                                {
+                                    layout
+                                        .position_map
+                                        .snapshot
+                                        .chars_at(cursor_position)
+                                        .next()
+                                        .and_then(|(character, _)| {
+                                            let text = character.to_string();
+                                            cx.text_system()
+                                                .layout_text(
+                                                    &text,
+                                                    cursor_row_layout.font_size,
+                                                    &[TextRun {
+                                                        len: text.len(),
+                                                        font: self.style.text.font(),
+                                                        color: self.style.background,
+                                                        underline: None,
+                                                    }],
+                                                    None,
+                                                )
+                                                .unwrap()
+                                                .pop()
+                                        })
+                                } else {
+                                    None
+                                };
+
+                                let x = cursor_character_x - layout.position_map.scroll_position.x;
+                                let y = cursor_position.row() as f32
+                                    * layout.position_map.line_height
+                                    - layout.position_map.scroll_position.y;
+                                if selection.is_newest {
+                                    editor.pixel_position_of_newest_cursor = Some(point(
+                                        text_bounds.origin.x + x + block_width / 2.,
+                                        text_bounds.origin.y
+                                            + y
+                                            + layout.position_map.line_height / 2.,
+                                    ));
+                                }
+                                cursors.push(Cursor {
+                                    color: selection_style.cursor,
+                                    block_width,
+                                    origin: point(x, y),
+                                    line_height: layout.position_map.line_height,
+                                    shape: selection.cursor_shape,
+                                    block_text,
+                                });
                             }
-                            cursors.push(Cursor {
-                                color: selection_style.cursor,
-                                block_width,
-                                origin: point(x, y),
-                                line_height: layout.position_map.line_height,
-                                shape: selection.cursor_shape,
-                                block_text,
-                            });
                         }
                     }
                 }
-            }
 
-            for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
-                let row = start_row + ix as u32;
-                line_with_invisibles.draw(
-                    layout,
-                    row,
-                    scroll_top,
-                    content_origin,
-                    scroll_left,
-                    whitespace_setting,
-                    &invisible_display_ranges,
-                    cx,
-                )
-            }
-
-            cx.with_z_index(0, |cx| {
-                for cursor in cursors {
-                    cursor.paint(content_origin, cx);
+                for (ix, line_with_invisibles) in
+                    layout.position_map.line_layouts.iter().enumerate()
+                {
+                    let row = start_row + ix as u32;
+                    line_with_invisibles.draw(
+                        layout,
+                        row,
+                        content_origin,
+                        whitespace_setting,
+                        &invisible_display_ranges,
+                        cx,
+                    )
                 }
-            });
 
-            if let Some((position, context_menu)) = layout.context_menu.as_mut() {
-                cx.with_z_index(1, |cx| {
-                    let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
-                    let available_space = size(
-                        AvailableSpace::MinContent,
-                        AvailableSpace::Definite(
-                            (12. * line_height).min((bounds.size.height - line_height) / 2.),
-                        ),
-                    );
-                    let context_menu_size = context_menu.measure(available_space, editor, cx);
-
-                    let cursor_row_layout = &layout.position_map.line_layouts
-                        [(position.row() - start_row) as usize]
-                        .line;
-                    let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
-                    let y =
-                        (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
-                    let mut list_origin = content_origin + point(x, y);
-                    let list_width = context_menu_size.width;
-                    let list_height = context_menu_size.height;
-
-                    // Snap the right edge of the list to the right edge of the window if
-                    // its horizontal bounds overflow.
-                    if list_origin.x + list_width > cx.viewport_size().width {
-                        list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
+                cx.with_z_index(0, |cx| {
+                    for cursor in cursors {
+                        cursor.paint(content_origin, cx);
                     }
+                });
 
-                    if list_origin.y + list_height > bounds.lower_right().y {
-                        list_origin.y -= layout.position_map.line_height - list_height;
-                    }
+                if let Some((position, context_menu)) = layout.context_menu.as_mut() {
+                    cx.with_z_index(1, |cx| {
+                        let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
+                        let available_space = size(
+                            AvailableSpace::MinContent,
+                            AvailableSpace::Definite(
+                                (12. * line_height)
+                                    .min((text_bounds.size.height - line_height) / 2.),
+                            ),
+                        );
+                        let context_menu_size = context_menu.measure(available_space, editor, cx);
+
+                        let cursor_row_layout = &layout.position_map.line_layouts
+                            [(position.row() - start_row) as usize]
+                            .line;
+                        let x = cursor_row_layout.x_for_index(position.column() as usize)
+                            - layout.position_map.scroll_position.x;
+                        let y = (position.row() + 1) as f32 * layout.position_map.line_height
+                            - layout.position_map.scroll_position.y;
+                        let mut list_origin = content_origin + point(x, y);
+                        let list_width = context_menu_size.width;
+                        let list_height = context_menu_size.height;
+
+                        // Snap the right edge of the list to the right edge of the window if
+                        // its horizontal bounds overflow.
+                        if list_origin.x + list_width > cx.viewport_size().width {
+                            list_origin.x =
+                                (cx.viewport_size().width - list_width).max(Pixels::ZERO);
+                        }
 
-                    context_menu.draw(list_origin, available_space, editor, cx);
-                })
-            }
+                        if list_origin.y + list_height > text_bounds.lower_right().y {
+                            list_origin.y -= layout.position_map.line_height - list_height;
+                        }
 
-            // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
-            //     cx.scene().push_stacking_context(None, None);
-
-            //     // This is safe because we check on layout whether the required row is available
-            //     let hovered_row_layout =
-            //         &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
-
-            //     // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
-            //     // height. This is the size we will use to decide whether to render popovers above or below
-            //     // the hovered line.
-            //     let first_size = hover_popovers[0].size();
-            //     let height_to_reserve = first_size.y
-            //         + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
-
-            //     // Compute Hovered Point
-            //     let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
-            //     let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
-            //     let hovered_point = content_origin + point(x, y);
-
-            //     if hovered_point.y - height_to_reserve > 0.0 {
-            //         // There is enough space above. Render popovers above the hovered point
-            //         let mut current_y = hovered_point.y;
-            //         for hover_popover in hover_popovers {
-            //             let size = hover_popover.size();
-            //             let mut popover_origin = point(hovered_point.x, current_y - size.y);
-
-            //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
-            //             if x_out_of_bounds < 0.0 {
-            //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
-            //             }
-
-            //             hover_popover.paint(
-            //                 popover_origin,
-            //                 Bounds::<Pixels>::from_points(
-            //                     gpui::Point::<Pixels>::zero(),
-            //                     point(f32::MAX, f32::MAX),
-            //                 ), // Let content bleed outside of editor
-            //                 editor,
-            //                 cx,
-            //             );
-
-            //             current_y = popover_origin.y - HOVER_POPOVER_GAP;
-            //         }
-            //     } else {
-            //         // There is not enough space above. Render popovers below the hovered point
-            //         let mut current_y = hovered_point.y + layout.position_map.line_height;
-            //         for hover_popover in hover_popovers {
-            //             let size = hover_popover.size();
-            //             let mut popover_origin = point(hovered_point.x, current_y);
-
-            //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
-            //             if x_out_of_bounds < 0.0 {
-            //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
-            //             }
-
-            //             hover_popover.paint(
-            //                 popover_origin,
-            //                 Bounds::<Pixels>::from_points(
-            //                     gpui::Point::<Pixels>::zero(),
-            //                     point(f32::MAX, f32::MAX),
-            //                 ), // Let content bleed outside of editor
-            //                 editor,
-            //                 cx,
-            //             );
-
-            //             current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP;
-            //         }
-            //     }
-
-            //     cx.scene().pop_stacking_context();
-            // }
-        })
+                        context_menu.draw(list_origin, available_space, editor, cx);
+                    })
+                }
+
+                // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
+                //     cx.scene().push_stacking_context(None, None);
+
+                //     // This is safe because we check on layout whether the required row is available
+                //     let hovered_row_layout =
+                //         &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+
+                //     // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+                //     // height. This is the size we will use to decide whether to render popovers above or below
+                //     // the hovered line.
+                //     let first_size = hover_popovers[0].size();
+                //     let height_to_reserve = first_size.y
+                //         + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
+
+                //     // Compute Hovered Point
+                //     let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
+                //     let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
+                //     let hovered_point = content_origin + point(x, y);
+
+                //     if hovered_point.y - height_to_reserve > 0.0 {
+                //         // There is enough space above. Render popovers above the hovered point
+                //         let mut current_y = hovered_point.y;
+                //         for hover_popover in hover_popovers {
+                //             let size = hover_popover.size();
+                //             let mut popover_origin = point(hovered_point.x, current_y - size.y);
+
+                //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
+                //             if x_out_of_bounds < 0.0 {
+                //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
+                //             }
+
+                //             hover_popover.paint(
+                //                 popover_origin,
+                //                 Bounds::<Pixels>::from_points(
+                //                     gpui::Point::<Pixels>::zero(),
+                //                     point(f32::MAX, f32::MAX),
+                //                 ), // Let content bleed outside of editor
+                //                 editor,
+                //                 cx,
+                //             );
+
+                //             current_y = popover_origin.y - HOVER_POPOVER_GAP;
+                //         }
+                //     } else {
+                //         // There is not enough space above. Render popovers below the hovered point
+                //         let mut current_y = hovered_point.y + layout.position_map.line_height;
+                //         for hover_popover in hover_popovers {
+                //             let size = hover_popover.size();
+                //             let mut popover_origin = point(hovered_point.x, current_y);
+
+                //             let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x);
+                //             if x_out_of_bounds < 0.0 {
+                //                 popover_origin.set_x(popover_origin.x + x_out_of_bounds);
+                //             }
+
+                //             hover_popover.paint(
+                //                 popover_origin,
+                //                 Bounds::<Pixels>::from_points(
+                //                     gpui::Point::<Pixels>::zero(),
+                //                     point(f32::MAX, f32::MAX),
+                //                 ), // Let content bleed outside of editor
+                //                 editor,
+                //                 cx,
+                //             );
+
+                //             current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP;
+                //         }
+                //     }
+
+                //     cx.scene().pop_stacking_context();
+                // }
+            },
+        )
     }
 
     fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
@@ -1130,8 +1164,6 @@ impl EditorElement {
         line_end_overshoot: Pixels,
         layout: &LayoutState,
         content_origin: gpui::Point<Pixels>,
-        scroll_top: Pixels,
-        scroll_left: Pixels,
         bounds: Bounds<Pixels>,
         cx: &mut ViewContext<Editor>,
     ) {
@@ -1150,7 +1182,7 @@ impl EditorElement {
                 corner_radius,
                 start_y: content_origin.y
                     + row_range.start as f32 * layout.position_map.line_height
-                    - scroll_top,
+                    - layout.position_map.scroll_position.y,
                 lines: row_range
                     .into_iter()
                     .map(|row| {
@@ -1160,17 +1192,17 @@ impl EditorElement {
                             start_x: if row == range.start.row() {
                                 content_origin.x
                                     + line_layout.x_for_index(range.start.column() as usize)
-                                    - scroll_left
+                                    - layout.position_map.scroll_position.x
                             } else {
-                                content_origin.x - scroll_left
+                                content_origin.x - layout.position_map.scroll_position.x
                             },
                             end_x: if row == range.end.row() {
                                 content_origin.x
                                     + line_layout.x_for_index(range.end.column() as usize)
-                                    - scroll_left
+                                    - layout.position_map.scroll_position.x
                             } else {
                                 content_origin.x + line_layout.width + line_end_overshoot
-                                    - scroll_left
+                                    - layout.position_map.scroll_position.x
                             },
                         }
                     })
@@ -1564,7 +1596,6 @@ impl EditorElement {
 
         let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
         let mut active_rows = BTreeMap::new();
-        let mut fold_ranges = Vec::new();
         let is_singleton = editor.is_singleton(cx);
 
         let highlighted_rows = editor.highlighted_rows();
@@ -1574,19 +1605,6 @@ impl EditorElement {
             cx.theme().colors(),
         );
 
-        fold_ranges.extend(
-            snapshot
-                .folds_in_range(start_anchor..end_anchor)
-                .map(|anchor| {
-                    let start = anchor.start.to_point(&snapshot.buffer_snapshot);
-                    (
-                        start.row,
-                        start.to_display_point(&snapshot.display_snapshot)
-                            ..anchor.end.to_display_point(&snapshot),
-                    )
-                }),
-        );
-
         let mut newest_selection_head = None;
 
         if editor.show_local_selections {
@@ -1684,36 +1702,17 @@ impl EditorElement {
             ShowScrollbar::Auto => {
                 // Git
                 (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
-                        ||
-                        // Selections
-                        (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
-                        // Scrollmanager
-                        || editor.scroll_manager.scrollbars_visible()
+                ||
+                // Selections
+                (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
+                // Scrollmanager
+                || editor.scroll_manager.scrollbars_visible()
             }
             ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
             ShowScrollbar::Always => true,
             ShowScrollbar::Never => false,
         };
 
-        let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Hsla)> = Vec::new();
-        // todo!()
-
-        // fold_ranges
-        // .into_iter()
-        // .map(|(id, fold)| {
-        //     // todo!("folds!")
-        //     // let color = self
-        //     //     .style
-        //     //     .folds
-        //     //     .ellipses
-        //     //     .background
-        //     //     .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
-        //     //     .color;
-
-        //     // (id, fold, color)
-        // })
-        // .collect();
-
         let head_for_relative = newest_selection_head.unwrap_or_else(|| {
             let newest = editor.selections.newest::<Point>(cx);
             SelectionLayout::new(
@@ -1754,21 +1753,23 @@ impl EditorElement {
             .width;
         let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width;
 
-        let (scroll_width, blocks) = self.layout_blocks(
-            start_row..end_row,
-            &snapshot,
-            bounds.size.width,
-            scroll_width,
-            gutter_padding,
-            gutter_width,
-            em_width,
-            gutter_width + gutter_margin,
-            line_height,
-            &style,
-            &line_layouts,
-            editor,
-            cx,
-        );
+        let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| {
+            self.layout_blocks(
+                start_row..end_row,
+                &snapshot,
+                bounds.size.width,
+                scroll_width,
+                gutter_padding,
+                gutter_width,
+                em_width,
+                gutter_width + gutter_margin,
+                line_height,
+                &style,
+                &line_layouts,
+                editor,
+                cx,
+            )
+        });
 
         let scroll_max = point(
             f32::from((scroll_width - text_size.width) / em_width).max(0.0),
@@ -1828,15 +1829,16 @@ impl EditorElement {
         // );
         // let mode = editor.mode;
 
-        // todo!("fold_indicators")
-        // let mut fold_indicators = editor.render_fold_indicators(
-        //     fold_statuses,
-        //     &style,
-        //     editor.gutter_hovered,
-        //     line_height,
-        //     gutter_margin,
-        //     cx,
-        // );
+        let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
+            editor.render_fold_indicators(
+                fold_statuses,
+                &style,
+                editor.gutter_hovered,
+                line_height,
+                gutter_margin,
+                cx,
+            )
+        });
 
         // todo!("context_menu")
         // if let Some((_, context_menu)) = context_menu.as_mut() {
@@ -1853,20 +1855,6 @@ impl EditorElement {
         //     );
         // }
 
-        // todo!("fold indicators")
-        // for fold_indicator in fold_indicators.iter_mut() {
-        //     if let Some(indicator) = fold_indicator.as_mut() {
-        //         indicator.layout(
-        //             SizeConstraint::strict_along(
-        //                 Axis::Vertical,
-        //                 line_height * style.code_actions.vertical_scale,
-        //             ),
-        //             editor,
-        //             cx,
-        //         );
-        //     }
-        // }
-
         // todo!("hover popovers")
         // if let Some((_, hover_popovers)) = hover.as_mut() {
         //     for hover_popover in hover_popovers.iter_mut() {
@@ -1926,6 +1914,10 @@ impl EditorElement {
             mode: editor_mode,
             position_map: Arc::new(PositionMap {
                 size: bounds.size,
+                scroll_position: point(
+                    scroll_position.x * em_width,
+                    scroll_position.y * line_height,
+                ),
                 scroll_max,
                 line_layouts,
                 line_height,
@@ -1933,6 +1925,7 @@ impl EditorElement {
                 em_advance,
                 snapshot,
             }),
+            visible_anchor_range: start_anchor..end_anchor,
             visible_display_row_range: start_row..end_row,
             wrap_guides,
             gutter_size,
@@ -1946,14 +1939,13 @@ impl EditorElement {
             active_rows,
             highlighted_rows,
             highlighted_ranges,
-            fold_ranges,
             line_number_layouts,
             display_hunks,
             blocks,
             selections,
             context_menu,
             code_actions_indicator,
-            // fold_indicators,
+            fold_indicators,
             tab_invisible,
             space_invisible,
             // hover_popovers: hover,
@@ -2012,14 +2004,13 @@ impl EditorElement {
                         anchor_x,
                         gutter_padding,
                         line_height,
-                        // scroll_x,
                         gutter_width,
                         em_width,
                         block_id,
+                        editor_style: &self.style,
                     })
                 }
                 TransformBlock::ExcerptHeader {
-                    id,
                     buffer,
                     range,
                     starts_new_buffer,
@@ -2041,9 +2032,7 @@ impl EditorElement {
                             .map_or(range.context.start, |primary| primary.start);
                         let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
 
-                        // todo!("avoid ElementId collision risk here")
-                        let icon_button_id: usize = id.clone().into();
-                        IconButton::new(icon_button_id, ui::Icon::ArrowUpRight)
+                        IconButton::new(block_id, ui::Icon::ArrowUpRight)
                             .on_click(move |editor: &mut Editor, cx| {
                                 editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
                             })
@@ -2139,11 +2128,13 @@ impl EditorElement {
         bounds: Bounds<Pixels>,
         gutter_bounds: Bounds<Pixels>,
         text_bounds: Bounds<Pixels>,
-        position_map: &Arc<PositionMap>,
+        layout: &LayoutState,
         cx: &mut ViewContext<Editor>,
     ) {
+        let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
+
         cx.on_mouse_event({
-            let position_map = position_map.clone();
+            let position_map = layout.position_map.clone();
             move |editor, event: &ScrollWheelEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
@@ -2155,7 +2146,7 @@ impl EditorElement {
             }
         });
         cx.on_mouse_event({
-            let position_map = position_map.clone();
+            let position_map = layout.position_map.clone();
             move |editor, event: &MouseDownEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
@@ -2167,7 +2158,7 @@ impl EditorElement {
             }
         });
         cx.on_mouse_event({
-            let position_map = position_map.clone();
+            let position_map = layout.position_map.clone();
             move |editor, event: &MouseUpEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
@@ -2180,7 +2171,7 @@ impl EditorElement {
         });
         // todo!()
         // on_down(MouseButton::Right, {
-        //     let position_map = position_map.clone();
+        //     let position_map = layout.position_map.clone();
         //     move |event, editor, cx| {
         //         if !Self::mouse_right_down(
         //             editor,
@@ -2194,7 +2185,7 @@ impl EditorElement {
         //     }
         // });
         cx.on_mouse_event({
-            let position_map = position_map.clone();
+            let position_map = layout.position_map.clone();
             move |editor, event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
@@ -2260,11 +2251,7 @@ impl LineWithInvisibles {
 
                 if !line_chunk.is_empty() && !line_exceeded_max_len {
                     let text_style = if let Some(style) = highlighted_chunk.style {
-                        text_style
-                            .clone()
-                            .highlight(style)
-                            .map(Cow::Owned)
-                            .unwrap_or_else(|_| Cow::Borrowed(text_style))
+                        Cow::Owned(text_style.clone().highlight(style))
                     } else {
                         Cow::Borrowed(text_style)
                     };

crates/editor2/src/git.rs 🔗

@@ -60,8 +60,8 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
     let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
     let folds_range = folds_start..folds_end;
 
-    let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
-        let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
+    let containing_fold = snapshot.folds_in_range(folds_range).find(|fold| {
+        let fold_point_range = fold.range.to_point(&snapshot.buffer_snapshot);
         let fold_point_range = fold_point_range.start..=fold_point_range.end;
 
         let folded_start = fold_point_range.contains(&hunk_start_point);
@@ -72,7 +72,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
     });
 
     if let Some(fold) = containing_fold {
-        let row = fold.start.to_display_point(snapshot).row();
+        let row = fold.range.start.to_display_point(snapshot).row();
         DisplayDiffHunk::Folded { display_row: row }
     } else {
         let start = hunk_start_point.to_display_point(snapshot).row();

crates/file_finder2/src/file_finder.rs 🔗

@@ -34,7 +34,7 @@ pub fn init(cx: &mut AppContext) {
 impl FileFinder {
     fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
         workspace.register_action(|workspace, _: &Toggle, cx| {
-            let Some(file_finder) = workspace.current_modal::<Self>(cx) else {
+            let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
                 Self::open(workspace, cx);
                 return;
             };
@@ -738,1236 +738,1089 @@ impl PickerDelegate for FileFinderDelegate {
     }
 }
 
-// #[cfg(test)]
-// mod tests {
-//     use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
-
-//     use super::*;
-//     use editor::Editor;
-//     use gpui::{Entity, TestAppContext, VisualTestContext};
-//     use menu::{Confirm, SelectNext};
-//     use serde_json::json;
-//     use workspace::{AppState, Workspace};
-
-//     #[ctor::ctor]
-//     fn init_logger() {
-//         if std::env::var("RUST_LOG").is_ok() {
-//             env_logger::init();
-//         }
-//     }
-
-//     #[gpui::test]
-//     async fn test_matching_paths(cx: &mut TestAppContext) {
-//         let app_state = init_test(cx);
-//         app_state
-//             .fs
-//             .as_fake()
-//             .insert_tree(
-//                 "/root",
-//                 json!({
-//                     "a": {
-//                         "banana": "",
-//                         "bandana": "",
-//                     }
-//                 }),
-//             )
-//             .await;
-
-//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-
-//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
-//         let cx = &mut cx;
-
-//         picker
-//             .update(cx, |picker, cx| {
-//                 picker.delegate.update_matches("bna".to_string(), cx)
-//             })
-//             .await;
-
-//         picker.update(cx, |picker, _| {
-//             assert_eq!(picker.delegate.matches.len(), 2);
-//         });
-
-//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
-//         cx.dispatch_action(SelectNext);
-//         cx.dispatch_action(Confirm);
-//         active_pane
-//             .condition(cx, |pane, _| pane.active_item().is_some())
-//             .await;
-//         cx.read(|cx| {
-//             let active_item = active_pane.read(cx).active_item().unwrap();
-//             assert_eq!(
-//                 active_item
-//                     .to_any()
-//                     .downcast::<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)
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use std::{assert_eq, 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, cx) = build_find_picker(project, cx);
+
+        cx.simulate_input("bna");
+
+        picker.update(cx, |picker, _| {
+            assert_eq!(picker.delegate.matches.len(), 2);
+        });
+
+        cx.dispatch_action(SelectNext);
+        cx.dispatch_action(Confirm);
+
+        cx.read(|cx| {
+            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
+            assert_eq!(active_editor.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, cx) = build_find_picker(project, 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));
+        });
+
+        cx.dispatch_action(SelectNext);
+        cx.dispatch_action(Confirm);
+
+        let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).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, cx) = build_find_picker(project, 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));
+        });
+
+        cx.dispatch_action(SelectNext);
+        cx.dispatch_action(Confirm);
+
+        let editor = cx.update(|cx| workspace.read(cx).active_item_as::<Editor>(cx).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, _, cx) = build_find_picker(project, 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, _, cx) = build_find_picker(project, 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, _, cx) = build_find_picker(project, 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 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().as_u64() as usize)
+        });
+
+        // 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 = ProjectPath {
+            worktree_id,
+            path: Arc::from(Path::new("/root/dir2/b.txt")),
+        };
+        workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path(b_path, None, true, cx)
+            })
+            .await
+            .unwrap();
+        let finder = open_file_picker(&workspace, cx);
+        finder
+            .update(cx, |f, cx| {
+                f.delegate.spawn_search(test_path_like("a.txt"), cx)
+            })
+            .await;
+
+        finder.update(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 (picker, _workspace, cx) = build_find_picker(project, cx);
+
+        picker
+            .update(cx, |f, cx| {
+                f.delegate.spawn_search(test_path_like("dir"), cx)
+            })
+            .await;
+        cx.read(|cx| {
+            let finder = picker.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, cx) = cx.add_window_view(|cx| Workspace::test_new(project, 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().as_u64() as usize)
+        });
+
+        // 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.
+        workspace.update(cx, |_, cx| dbg!(cx.focused()));
+
+        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, cx) = cx.add_window_view(|cx| Workspace::test_new(project, 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().as_u64() as usize)
+        });
+        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().as_u64() as usize != worktree_id.to_usize()
+                    })
+                    .expect("New worktree should have a different id")
+                    .entity_id()
+                    .as_u64() as usize,
+            )
+        });
+        cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
+
+        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, cx) = cx.add_window_view(|cx| Workspace::test_new(project, 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 picker = active_file_picker(&workspace, cx);
+            let selected_index = picker.update(cx, |picker, _| picker.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
+                .active_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, cx) = cx.add_window_view(|cx| Workspace::test_new(project, 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().as_u64() as usize)
+        });
+
+        // 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;
+
+        let finder = open_file_picker(&workspace, cx);
+        let first_query = "f";
+        finder
+            .update(cx, |finder, cx| {
+                finder.delegate.update_matches(first_query.to_string(), cx)
+            })
+            .await;
+        finder.update(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 = active_file_picker(&workspace, cx);
+        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 = active_file_picker(&workspace, cx);
+        finder
+            .update(cx, |finder, cx| {
+                finder
+                    .delegate
+                    .update_matches(first_query_again.to_string(), cx)
+            })
+            .await;
+        finder.update(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, cx) = cx.add_window_view(|cx| Workspace::test_new(project, 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;
+
+        let finder = open_file_picker(&workspace, cx);
+        let query = "collab_ui";
+        cx.simulate_input(query);
+        finder.update(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, cx) = cx.add_window_view(|cx| Workspace::test_new(project, 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;
+
+        let picker = open_file_picker(&workspace, cx);
+        cx.simulate_input("rs");
+
+        picker.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> {
+        let picker = open_file_picker(&workspace, cx);
+        cx.simulate_input(input);
+
+        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()
+        });
+
+        cx.dispatch_action(SelectNext);
+        cx.dispatch_action(Confirm);
+
+        cx.read(|cx| {
+            let active_editor = workspace.read(cx).active_item_as::<Editor>(cx).unwrap();
+            let active_editor_title = active_editor.read(cx).title(cx);
+            assert_eq!(
+                expected_editor_title, active_editor_title,
+                "Unexpected editor title for query {input}"
+            );
+        });
+
+        cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
+
+        history_items
+    }
+
+    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 build_find_picker(
+        project: Model<Project>,
+        cx: &mut TestAppContext,
+    ) -> (
+        View<Picker<FileFinderDelegate>>,
+        View<Workspace>,
+        &mut VisualTestContext,
+    ) {
+        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+        let picker = open_file_picker(&workspace, cx);
+        (picker, workspace, cx)
+    }
+
+    #[track_caller]
+    fn open_file_picker(
+        workspace: &View<Workspace>,
+        cx: &mut VisualTestContext,
+    ) -> View<Picker<FileFinderDelegate>> {
+        cx.dispatch_action(Toggle);
+        active_file_picker(workspace, cx)
+    }
+
+    #[track_caller]
+    fn active_file_picker(
+        workspace: &View<Workspace>,
+        cx: &mut VisualTestContext,
+    ) -> View<Picker<FileFinderDelegate>> {
+        workspace.update(cx, |workspace, cx| {
+            workspace
+                .active_modal::<FileFinder>(cx)
+                .unwrap()
+                .read(cx)
+                .picker
+                .clone()
+        })
+    }
+}

crates/gpui2/docs/contexts.md 🔗

@@ -0,0 +1,41 @@
+# Contexts
+
+GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services.
+
+There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead.
+
+```
+     AppContext
+     /        \
+ModelContext  WindowContext
+              /
+        ViewContext
+```
+
+- The `AppContext` forms the root of the hierarchy
+- `ModelContext` and `WindowContext` both dereference to `AppContext`
+- `ViewContext` dereferences to `WindowContext`
+
+## `AppContext`
+
+Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model<T>` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`...
+
+## `WindowContext`
+
+Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`.
+
+## `ModelContext<T>`
+
+Available when you create or update a `Model<T>`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events.
+
+## `ViewContext<V>`
+
+Available when you create or update a `View<V>`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events.
+
+## `AsyncAppContext` and `AsyncWindowContext`
+
+Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
+
+## `TestAppContext` and `TestVisualContext`
+
+These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.

crates/gpui2/src/action.rs 🔗

@@ -54,6 +54,9 @@ pub trait Action: std::fmt::Debug + 'static {
     where
         Self: Sized;
     fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+    where
+        Self: Sized;
+    fn is_registered() -> bool
     where
         Self: Sized;
 
@@ -88,6 +91,14 @@ where
         Ok(Box::new(action))
     }
 
+    fn is_registered() -> bool {
+        ACTION_REGISTRY
+            .read()
+            .names_by_type_id
+            .get(&TypeId::of::<A>())
+            .is_some()
+    }
+
     fn partial_eq(&self, action: &dyn Action) -> bool {
         action
             .as_any()

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

@@ -1,8 +1,8 @@
 use crate::{
     div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
     BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
-    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
-    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
+    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
+    View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -140,7 +140,7 @@ impl TestAppContext {
         .any_handle
     }
 
-    pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
+    pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
     where
         F: FnOnce(&mut ViewContext<V>) -> V,
         V: Render,
@@ -149,7 +149,9 @@ impl TestAppContext {
         let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
         drop(cx);
         let view = window.root_view(self).unwrap();
-        (view, VisualTestContext::from_window(*window.deref(), self))
+        let cx = Box::new(VisualTestContext::from_window(*window.deref(), self));
+        // it might be nice to try and cleanup these at the end of each test.
+        (view, Box::leak(cx))
     }
 
     pub fn simulate_new_path_selection(
@@ -220,7 +222,35 @@ impl TestAppContext {
     {
         window
             .update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
-            .unwrap()
+            .unwrap();
+
+        self.background_executor.run_until_parked()
+    }
+
+    /// simulate_keystrokes takes a space-separated list of keys to type.
+    /// cx.simulate_keystrokes("cmd-shift-p b k s p enter")
+    /// will run backspace on the current editor through the command palette.
+    pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) {
+        for keystroke in keystrokes
+            .split(" ")
+            .map(Keystroke::parse)
+            .map(Result::unwrap)
+        {
+            self.dispatch_keystroke(window, keystroke.into(), false);
+        }
+
+        self.background_executor.run_until_parked()
+    }
+
+    /// simulate_input takes a string of text to type.
+    /// cx.simulate_input("abc")
+    /// will type abc into your current editor.
+    pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
+        for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
+            self.dispatch_keystroke(window, keystroke.into(), false);
+        }
+
+        self.background_executor.run_until_parked()
     }
 
     pub fn dispatch_keystroke(
@@ -229,15 +259,41 @@ impl TestAppContext {
         keystroke: Keystroke,
         is_held: bool,
     ) {
+        let keystroke2 = keystroke.clone();
         let handled = window
             .update(self, |_, cx| {
                 cx.dispatch_event(InputEvent::KeyDown(KeyDownEvent { keystroke, is_held }))
             })
             .is_ok_and(|handled| handled);
-
-        if !handled {
-            // todo!() simluate input here
+        if handled {
+            return;
         }
+
+        let input_handler = self.update_test_window(window, |window| window.input_handler.clone());
+        let Some(input_handler) = input_handler else {
+            panic!(
+                "dispatch_keystroke {:?} failed to dispatch action or input",
+                &keystroke2
+            );
+        };
+        let text = keystroke2.ime_key.unwrap_or(keystroke2.key);
+        input_handler.lock().replace_text_in_range(None, &text);
+    }
+
+    pub fn update_test_window<R>(
+        &mut self,
+        window: AnyWindowHandle,
+        f: impl FnOnce(&mut TestWindow) -> R,
+    ) -> R {
+        window
+            .update(self, |_, cx| {
+                f(cx.window
+                    .platform_window
+                    .as_any_mut()
+                    .downcast_mut::<TestWindow>()
+                    .unwrap())
+            })
+            .unwrap()
     }
 
     pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
@@ -401,12 +457,24 @@ impl<'a> VisualTestContext<'a> {
         Self { cx, window }
     }
 
+    pub fn run_until_parked(&self) {
+        self.cx.background_executor.run_until_parked();
+    }
+
     pub fn dispatch_action<A>(&mut self, action: A)
     where
         A: Action,
     {
         self.cx.dispatch_action(self.window, action)
     }
+
+    pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
+        self.cx.simulate_keystrokes(self.window, keystrokes)
+    }
+
+    pub fn simulate_input(&mut self, input: &str) {
+        self.cx.simulate_input(self.window, input)
+    }
 }
 
 impl<'a> Context for VisualTestContext<'a> {

crates/gpui2/src/element.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
 };
 use derive_more::{Deref, DerefMut};
 pub(crate) use smallvec::SmallVec;
-use std::{any::Any, mem};
+use std::{any::Any, fmt::Debug, mem};
 
 pub trait Element<V: 'static> {
     type ElementState: 'static;
@@ -33,6 +33,42 @@ pub trait Element<V: 'static> {
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     );
+
+    fn draw<T, R>(
+        self,
+        origin: Point<Pixels>,
+        available_space: Size<T>,
+        view_state: &mut V,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
+    ) -> R
+    where
+        Self: Sized,
+        T: Clone + Default + Debug + Into<AvailableSpace>,
+    {
+        let mut element = RenderedElement {
+            element: self,
+            phase: ElementRenderPhase::Start,
+        };
+        element.draw(origin, available_space.map(Into::into), view_state, cx);
+        if let ElementRenderPhase::Painted { frame_state } = &element.phase {
+            if let Some(frame_state) = frame_state.as_ref() {
+                f(&frame_state, cx)
+            } else {
+                let element_id = element
+                    .element
+                    .element_id()
+                    .expect("we either have some frame_state or some element_id");
+                cx.with_element_state(element_id, |element_state, cx| {
+                    let element_state = element_state.unwrap();
+                    let result = f(&element_state, cx);
+                    (result, element_state)
+                })
+            }
+        } else {
+            unreachable!()
+        }
+    }
 }
 
 #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
@@ -100,7 +136,9 @@ enum ElementRenderPhase<V> {
         available_space: Size<AvailableSpace>,
         frame_state: Option<V>,
     },
-    Painted,
+    Painted {
+        frame_state: Option<V>,
+    },
 }
 
 /// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
@@ -162,7 +200,7 @@ where
             ElementRenderPhase::Start => panic!("must call initialize before layout"),
             ElementRenderPhase::LayoutRequested { .. }
             | ElementRenderPhase::LayoutComputed { .. }
-            | ElementRenderPhase::Painted => {
+            | ElementRenderPhase::Painted { .. } => {
                 panic!("element rendered twice")
             }
         };
@@ -197,7 +235,7 @@ where
                     self.element
                         .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
                 }
-                ElementRenderPhase::Painted
+                ElementRenderPhase::Painted { frame_state }
             }
 
             _ => panic!("must call layout before paint"),

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

@@ -6,15 +6,15 @@ use crate::{
     SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
 };
 use collections::HashMap;
-use parking_lot::Mutex;
 use refineable::Refineable;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
+    cell::RefCell,
     fmt::Debug,
     marker::PhantomData,
     mem,
-    sync::Arc,
+    rc::Rc,
     time::Duration,
 };
 use taffy::style::Overflow;
@@ -229,6 +229,20 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
         mut self,
         listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
     ) -> Self {
+        // NOTE: this debug assert has the side-effect of working around
+        // a bug where a crate consisting only of action definitions does
+        // not register the actions in debug builds:
+        //
+        // https://github.com/rust-lang/rust/issues/47384
+        // https://github.com/mmastrac/rust-ctor/issues/280
+        //
+        // if we are relying on this side-effect still, removing the debug_assert!
+        // likely breaks the command_palette tests.
+        debug_assert!(
+            A::is_registered(),
+            "{:?} is not registered as an action",
+            A::qualified_name()
+        );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, action, phase, cx| {
@@ -406,7 +420,7 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
             self.interactivity().tooltip_builder.is_none(),
             "calling tooltip more than once on the same element is not supported"
         );
-        self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
+        self.interactivity().tooltip_builder = Some(Rc::new(move |view_state, cx| {
             build_tooltip(view_state, cx).into()
         }));
 
@@ -555,7 +569,7 @@ type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
 
 pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
 
-pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
 
 pub type KeyDownListener<V> =
     Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
@@ -597,7 +611,7 @@ impl<V: 'static> ParentComponent<V> for Div<V> {
 }
 
 impl<V: 'static> Element<V> for Div<V> {
-    type ElementState = NodeState;
+    type ElementState = DivState;
 
     fn element_id(&self) -> Option<ElementId> {
         self.interactivity.element_id.clone()
@@ -617,7 +631,7 @@ impl<V: 'static> Element<V> for Div<V> {
             child.initialize(view_state, cx);
         }
 
-        NodeState {
+        DivState {
             interactive_state,
             child_layout_ids: SmallVec::new(),
         }
@@ -706,11 +720,17 @@ impl<V: 'static> Component<V> for Div<V> {
     }
 }
 
-pub struct NodeState {
+pub struct DivState {
     child_layout_ids: SmallVec<[LayoutId; 4]>,
     interactive_state: InteractiveElementState,
 }
 
+impl DivState {
+    pub fn is_active(&self) -> bool {
+        self.interactive_state.pending_mouse_down.borrow().is_some()
+    }
+}
+
 pub struct Interactivity<V> {
     pub element_id: Option<ElementId>,
     pub key_context: KeyContext,
@@ -876,7 +896,7 @@ where
 
         if !click_listeners.is_empty() || drag_listener.is_some() {
             let pending_mouse_down = element_state.pending_mouse_down.clone();
-            let mouse_down = pending_mouse_down.lock().clone();
+            let mouse_down = pending_mouse_down.borrow().clone();
             if let Some(mouse_down) = mouse_down {
                 if let Some(drag_listener) = drag_listener {
                     let active_state = element_state.clicked_state.clone();
@@ -890,7 +910,7 @@ where
                             && bounds.contains_point(&event.position)
                             && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
                         {
-                            *active_state.lock() = ElementClickedState::default();
+                            *active_state.borrow_mut() = ElementClickedState::default();
                             let cursor_offset = event.position - bounds.origin;
                             let drag = drag_listener(view_state, cursor_offset, cx);
                             cx.active_drag = Some(drag);
@@ -910,12 +930,14 @@ where
                             listener(view_state, &mouse_click, cx);
                         }
                     }
-                    *pending_mouse_down.lock() = None;
+                    *pending_mouse_down.borrow_mut() = None;
+                    cx.notify();
                 });
             } else {
-                cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
+                cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
                     if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                        *pending_mouse_down.lock() = Some(event.clone());
+                        *pending_mouse_down.borrow_mut() = Some(event.clone());
+                        cx.notify();
                     }
                 });
             }
@@ -930,8 +952,8 @@ where
                     return;
                 }
                 let is_hovered =
-                    bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
-                let mut was_hovered = was_hovered.lock();
+                    bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
+                let mut was_hovered = was_hovered.borrow_mut();
 
                 if is_hovered != was_hovered.clone() {
                     *was_hovered = is_hovered;
@@ -952,13 +974,13 @@ where
                 }
 
                 let is_hovered =
-                    bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
+                    bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none();
                 if !is_hovered {
-                    active_tooltip.lock().take();
+                    active_tooltip.borrow_mut().take();
                     return;
                 }
 
-                if active_tooltip.lock().is_none() {
+                if active_tooltip.borrow().is_none() {
                     let task = cx.spawn({
                         let active_tooltip = active_tooltip.clone();
                         let tooltip_builder = tooltip_builder.clone();
@@ -966,7 +988,7 @@ where
                         move |view, mut cx| async move {
                             cx.background_executor().timer(TOOLTIP_DELAY).await;
                             view.update(&mut cx, move |view_state, cx| {
-                                active_tooltip.lock().replace(ActiveTooltip {
+                                active_tooltip.borrow_mut().replace(ActiveTooltip {
                                     waiting: None,
                                     tooltip: Some(AnyTooltip {
                                         view: tooltip_builder(view_state, cx),
@@ -978,14 +1000,14 @@ where
                             .ok();
                         }
                     });
-                    active_tooltip.lock().replace(ActiveTooltip {
+                    active_tooltip.borrow_mut().replace(ActiveTooltip {
                         waiting: Some(task),
                         tooltip: None,
                     });
                 }
             });
 
-            if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
+            if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
                 if active_tooltip.tooltip.is_some() {
                     cx.active_tooltip = active_tooltip.tooltip.clone()
                 }
@@ -993,10 +1015,10 @@ where
         }
 
         let active_state = element_state.clicked_state.clone();
-        if !active_state.lock().is_clicked() {
+        if !active_state.borrow().is_clicked() {
             cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
-                    *active_state.lock() = ElementClickedState::default();
+                    *active_state.borrow_mut() = ElementClickedState::default();
                     cx.notify();
                 }
             });
@@ -1011,7 +1033,7 @@ where
                         .map_or(false, |bounds| bounds.contains_point(&down.position));
                     let element = bounds.contains_point(&down.position);
                     if group || element {
-                        *active_state.lock() = ElementClickedState { group, element };
+                        *active_state.borrow_mut() = ElementClickedState { group, element };
                         cx.notify();
                     }
                 }
@@ -1022,14 +1044,14 @@ where
         if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
             let scroll_offset = element_state
                 .scroll_offset
-                .get_or_insert_with(Arc::default)
+                .get_or_insert_with(Rc::default)
                 .clone();
             let line_height = cx.line_height();
             let scroll_max = (content_size - bounds.size).max(&Size::default());
 
             cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
                 if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    let mut scroll_offset = scroll_offset.lock();
+                    let mut scroll_offset = scroll_offset.borrow_mut();
                     let old_scroll_offset = *scroll_offset;
                     let delta = event.delta.pixel_delta(line_height);
 
@@ -1058,7 +1080,7 @@ where
         let scroll_offset = element_state
             .scroll_offset
             .as_ref()
-            .map(|scroll_offset| *scroll_offset.lock());
+            .map(|scroll_offset| *scroll_offset.borrow());
 
         cx.with_key_dispatch(
             self.key_context.clone(),
@@ -1157,7 +1179,7 @@ where
             }
         }
 
-        let clicked_state = element_state.clicked_state.lock();
+        let clicked_state = element_state.clicked_state.borrow();
         if clicked_state.group {
             if let Some(group) = self.group_active_style.as_ref() {
                 style.refine(&group.style)
@@ -1211,11 +1233,11 @@ impl<V: 'static> Default for Interactivity<V> {
 #[derive(Default)]
 pub struct InteractiveElementState {
     pub focus_handle: Option<FocusHandle>,
-    pub clicked_state: Arc<Mutex<ElementClickedState>>,
-    pub hover_state: Arc<Mutex<bool>>,
-    pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
-    pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
-    pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+    pub clicked_state: Rc<RefCell<ElementClickedState>>,
+    pub hover_state: Rc<RefCell<bool>>,
+    pub pending_mouse_down: Rc<RefCell<Option<MouseDownEvent>>>,
+    pub scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
+    pub active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
 }
 
 pub struct ActiveTooltip {

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

@@ -3,9 +3,8 @@ use crate::{
     ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
     Point, Size, StyleRefinement, Styled, ViewContext,
 };
-use parking_lot::Mutex;
 use smallvec::SmallVec;
-use std::{cmp, mem, ops::Range, sync::Arc};
+use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
 use taffy::style::Overflow;
 
 /// uniform_list provides lazy rendering for a set of items that are of uniform height.
@@ -61,23 +60,23 @@ pub struct UniformList<V: 'static> {
 }
 
 #[derive(Clone, Default)]
-pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
+pub struct UniformListScrollHandle(Rc<RefCell<Option<ScrollHandleState>>>);
 
 #[derive(Clone, Debug)]
 struct ScrollHandleState {
     item_height: Pixels,
     list_height: Pixels,
-    scroll_offset: Arc<Mutex<Point<Pixels>>>,
+    scroll_offset: Rc<RefCell<Point<Pixels>>>,
 }
 
 impl UniformListScrollHandle {
     pub fn new() -> Self {
-        Self(Arc::new(Mutex::new(None)))
+        Self(Rc::new(RefCell::new(None)))
     }
 
     pub fn scroll_to_item(&self, ix: usize) {
-        if let Some(state) = &*self.0.lock() {
-            let mut scroll_offset = state.scroll_offset.lock();
+        if let Some(state) = &*self.0.borrow() {
+            let mut scroll_offset = state.scroll_offset.borrow_mut();
             let item_top = state.item_height * ix;
             let item_bottom = item_top + state.item_height;
             let scroll_top = -scroll_offset.y;
@@ -196,7 +195,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
         let shared_scroll_offset = element_state
             .interactive
             .scroll_offset
-            .get_or_insert_with(Arc::default)
+            .get_or_insert_with(Rc::default)
             .clone();
 
         interactivity.paint(
@@ -222,7 +221,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
                             .measure_item(view_state, Some(padded_bounds.size.width), cx)
                             .height;
                         if let Some(scroll_handle) = self.scroll_handle.clone() {
-                            scroll_handle.0.lock().replace(ScrollHandleState {
+                            scroll_handle.0.borrow_mut().replace(ScrollHandleState {
                                 item_height,
                                 list_height: padded_bounds.size.height,
                                 scroll_offset: shared_scroll_offset,

crates/gpui2/src/interactive.rs 🔗

@@ -337,7 +337,6 @@ mod test {
             .update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
             .unwrap();
 
-        cx.dispatch_keystroke(*window, Keystroke::parse("space").unwrap(), false);
         cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
 
         window

crates/gpui2/src/key_dispatch.rs 🔗

@@ -60,7 +60,7 @@ impl DispatchTree {
         self.keystroke_matchers.clear();
     }
 
-    pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) {
+    pub fn push_node(&mut self, context: KeyContext) {
         let parent = self.node_stack.last().copied();
         let node_id = DispatchNodeId(self.nodes.len());
         self.nodes.push(DispatchNode {
@@ -71,12 +71,6 @@ impl DispatchTree {
         if !context.is_empty() {
             self.active_node().context = context.clone();
             self.context_stack.push(context);
-            if let Some((context_stack, matcher)) = old_dispatcher
-                .keystroke_matchers
-                .remove_entry(self.context_stack.as_slice())
-            {
-                self.keystroke_matchers.insert(context_stack, matcher);
-            }
         }
     }
 
@@ -87,6 +81,33 @@ impl DispatchTree {
         }
     }
 
+    pub fn clear_keystroke_matchers(&mut self) {
+        self.keystroke_matchers.clear();
+    }
+
+    /// Preserve keystroke matchers from previous frames to support multi-stroke
+    /// bindings across multiple frames.
+    pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option<FocusId>) {
+        if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) {
+            let dispatch_path = self.dispatch_path(node_id);
+
+            self.context_stack.clear();
+            for node_id in dispatch_path {
+                let node = self.node(node_id);
+                if !node.context.is_empty() {
+                    self.context_stack.push(node.context.clone());
+                }
+
+                if let Some((context_stack, matcher)) = old_tree
+                    .keystroke_matchers
+                    .remove_entry(self.context_stack.as_slice())
+                {
+                    self.keystroke_matchers.insert(context_stack, matcher);
+                }
+            }
+        }
+    }
+
     pub fn on_key_event(&mut self, listener: KeyListener) {
         self.active_node().key_listeners.push(listener);
     }

crates/gpui2/src/platform.rs 🔗

@@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync {
     fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
     fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
     fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
-    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)>;
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        raster_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
     fn wrap_line(
         &self,

crates/gpui2/src/platform/mac/text_system.rs 🔗

@@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem {
                 },
             )?;
 
-            Ok(candidates[ix])
+            let font_id = candidates[ix];
+            lock.font_selections.insert(font.clone(), font_id);
+            Ok(font_id)
         }
     }
 
@@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem {
     fn rasterize_glyph(
         &self,
         glyph_id: &RenderGlyphParams,
+        raster_bounds: Bounds<DevicePixels>,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        self.0.read().rasterize_glyph(glyph_id)
+        self.0.read().rasterize_glyph(glyph_id, raster_bounds)
     }
 
     fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
@@ -247,8 +250,11 @@ impl MacTextSystemState {
             .into())
     }
 
-    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        let glyph_bounds = self.raster_bounds(params)?;
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        glyph_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
         if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
             Err(anyhow!("glyph bounds are empty"))
         } else {
@@ -260,6 +266,7 @@ impl MacTextSystemState {
             if params.subpixel_variant.y > 0 {
                 bitmap_size.height += DevicePixels(1);
             }
+            let bitmap_size = bitmap_size;
 
             let mut bytes;
             let cx;

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

@@ -22,7 +22,7 @@ pub struct TestWindow {
     bounds: WindowBounds,
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
-    input_handler: Option<Box<dyn PlatformInputHandler>>,
+    pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
     handlers: Mutex<Handlers>,
     platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
@@ -80,11 +80,11 @@ impl PlatformWindow for TestWindow {
     }
 
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
-        todo!()
+        self
     }
 
     fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
-        self.input_handler = Some(input_handler);
+        self.input_handler = Some(Arc::new(Mutex::new(input_handler)));
     }
 
     fn prompt(

crates/gpui2/src/style.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
     Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
-    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result,
-    Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
+    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
+    SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
 };
 use refineable::{Cascade, Refineable};
 use smallvec::SmallVec;
@@ -157,7 +157,7 @@ impl Default for TextStyle {
 }
 
 impl TextStyle {
-    pub fn highlight(mut self, style: HighlightStyle) -> Result<Self> {
+    pub fn highlight(mut self, style: HighlightStyle) -> Self {
         if let Some(weight) = style.font_weight {
             self.font_weight = weight;
         }
@@ -177,7 +177,7 @@ impl TextStyle {
             self.underline = Some(underline);
         }
 
-        Ok(self)
+        self
     }
 
     pub fn font(&self) -> Font {

crates/gpui2/src/text_system.rs 🔗

@@ -39,6 +39,7 @@ pub struct TextSystem {
     platform_text_system: Arc<dyn PlatformTextSystem>,
     font_ids_by_font: RwLock<HashMap<Font, FontId>>,
     font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
+    raster_bounds: RwLock<HashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
     wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
     font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
 }
@@ -48,10 +49,11 @@ impl TextSystem {
         TextSystem {
             line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
             platform_text_system,
-            font_metrics: RwLock::new(HashMap::default()),
-            font_ids_by_font: RwLock::new(HashMap::default()),
-            wrapper_pool: Mutex::new(HashMap::default()),
-            font_runs_pool: Default::default(),
+            font_metrics: RwLock::default(),
+            raster_bounds: RwLock::default(),
+            font_ids_by_font: RwLock::default(),
+            wrapper_pool: Mutex::default(),
+            font_runs_pool: Mutex::default(),
         }
     }
 
@@ -252,14 +254,24 @@ impl TextSystem {
     }
 
     pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
-        self.platform_text_system.glyph_raster_bounds(params)
+        let raster_bounds = self.raster_bounds.upgradable_read();
+        if let Some(bounds) = raster_bounds.get(params) {
+            Ok(bounds.clone())
+        } else {
+            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
+            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
+            raster_bounds.insert(params.clone(), bounds);
+            Ok(bounds)
+        }
     }
 
     pub fn rasterize_glyph(
         &self,
-        glyph_id: &RenderGlyphParams,
+        params: &RenderGlyphParams,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        self.platform_text_system.rasterize_glyph(glyph_id)
+        let raster_bounds = self.raster_bounds(params)?;
+        self.platform_text_system
+            .rasterize_glyph(params, raster_bounds)
     }
 }
 

crates/gpui2/src/view.rs 🔗

@@ -63,6 +63,16 @@ impl<V: 'static> View<V> {
     pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V {
         self.model.read(cx)
     }
+
+    pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
+    where
+        C: 'static + Component<V>,
+    {
+        RenderViewWith {
+            view: self.clone(),
+            component: Some(component),
+        }
+    }
 }
 
 impl<V> Clone for View<V> {
@@ -281,6 +291,67 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
 //     }
 // }
 
+pub struct RenderViewWith<C, V> {
+    view: View<V>,
+    component: Option<C>,
+}
+
+impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
+where
+    C: 'static + Component<ViewState>,
+    ParentViewState: 'static,
+    ViewState: 'static,
+{
+    fn render(self) -> AnyElement<ParentViewState> {
+        AnyElement::new(self)
+    }
+}
+
+impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
+where
+    C: 'static + Component<ViewState>,
+    ParentViewState: 'static,
+    ViewState: 'static,
+{
+    type ElementState = AnyElement<ViewState>;
+
+    fn element_id(&self) -> Option<ElementId> {
+        Some(self.view.entity_id().into())
+    }
+
+    fn initialize(
+        &mut self,
+        _: &mut ParentViewState,
+        _: Option<Self::ElementState>,
+        cx: &mut ViewContext<ParentViewState>,
+    ) -> Self::ElementState {
+        self.view.update(cx, |view, cx| {
+            let mut element = self.component.take().unwrap().render();
+            element.initialize(view, cx);
+            element
+        })
+    }
+
+    fn layout(
+        &mut self,
+        _: &mut ParentViewState,
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
+    ) -> LayoutId {
+        self.view.update(cx, |view, cx| element.layout(view, cx))
+    }
+
+    fn paint(
+        &mut self,
+        _: Bounds<Pixels>,
+        _: &mut ParentViewState,
+        element: &mut Self::ElementState,
+        cx: &mut ViewContext<ParentViewState>,
+    ) {
+        self.view.update(cx, |view, cx| element.paint(view, cx))
+    }
+}
+
 mod any_view {
     use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
     use std::any::Any;

crates/gpui2/src/window.rs 🔗

@@ -189,7 +189,7 @@ impl Drop for FocusHandle {
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
     pub(crate) removed: bool,
-    platform_window: Box<dyn PlatformWindow>,
+    pub(crate) platform_window: Box<dyn PlatformWindow>,
     display_id: DisplayId,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     rem_size: Pixels,
@@ -216,7 +216,7 @@ pub struct Window {
 
 // #[derive(Default)]
 pub(crate) struct Frame {
-    element_states: HashMap<GlobalElementId, AnyBox>,
+    pub(crate) element_states: HashMap<GlobalElementId, AnyBox>,
     mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
     pub(crate) dispatch_tree: DispatchTree,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
@@ -393,6 +393,10 @@ impl<'a> WindowContext<'a> {
 
     /// Move focus to the element associated with the given `FocusHandle`.
     pub fn focus(&mut self, handle: &FocusHandle) {
+        if self.window.focus == Some(handle.id) {
+            return;
+        }
+
         let focus_id = handle.id;
 
         if self.window.last_blur.is_none() {
@@ -400,6 +404,10 @@ impl<'a> WindowContext<'a> {
         }
 
         self.window.focus = Some(focus_id);
+        self.window
+            .current_frame
+            .dispatch_tree
+            .clear_keystroke_matchers();
         self.app.push_effect(Effect::FocusChanged {
             window_handle: self.window.handle,
             focused: Some(focus_id),
@@ -1091,6 +1099,14 @@ impl<'a> WindowContext<'a> {
             });
         }
 
+        self.window
+            .current_frame
+            .dispatch_tree
+            .preserve_keystroke_matchers(
+                &mut self.window.previous_frame.dispatch_tree,
+                self.window.focus,
+            );
+
         self.window.root_view = Some(root_view);
         let scene = self.window.current_frame.scene_builder.build();
 
@@ -2093,7 +2109,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         window
             .current_frame
             .dispatch_tree
-            .push_node(context.clone(), &mut window.previous_frame.dispatch_tree);
+            .push_node(context.clone());
         if let Some(focus_handle) = focus_handle.as_ref() {
             window
                 .current_frame
@@ -2471,7 +2487,7 @@ impl From<SmallVec<[u32; 16]>> for StackingOrder {
 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
 pub enum ElementId {
     View(EntityId),
-    Number(usize),
+    Integer(usize),
     Name(SharedString),
     FocusHandle(FocusId),
 }
@@ -2496,13 +2512,13 @@ impl From<EntityId> for ElementId {
 
 impl From<usize> for ElementId {
     fn from(id: usize) -> Self {
-        ElementId::Number(id)
+        ElementId::Integer(id)
     }
 }
 
 impl From<i32> for ElementId {
     fn from(id: i32) -> Self {
-        Self::Number(id as usize)
+        Self::Integer(id as usize)
     }
 }
 

crates/project/src/project.rs 🔗

@@ -1709,6 +1709,7 @@ impl Project {
                     self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
                 };
 
+                let project_path = project_path.clone();
                 cx.spawn(move |this, mut cx| async move {
                     let load_result = load_buffer.await;
                     *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
@@ -1726,7 +1727,7 @@ impl Project {
         cx.foreground().spawn(async move {
             wait_for_loading_buffer(loading_watch)
                 .await
-                .map_err(|error| anyhow!("{}", error))
+                .map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
         })
     }
 

crates/project/src/worktree.rs 🔗

@@ -3694,7 +3694,7 @@ impl BackgroundScanner {
                 }
                 Err(err) => {
                     // TODO - create a special 'error' entry in the entries tree to mark this
-                    log::error!("error reading file on event {:?}", err);
+                    log::error!("error reading file {abs_path:?} on event: {err:#}");
                 }
             }
         }

crates/project2/src/project2.rs 🔗

@@ -1741,6 +1741,7 @@ impl Project {
                     self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
                 };
 
+                let project_path = project_path.clone();
                 cx.spawn(move |this, mut cx| async move {
                     let load_result = load_buffer.await;
                     *tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
@@ -1759,7 +1760,7 @@ impl Project {
         cx.background_executor().spawn(async move {
             wait_for_loading_buffer(loading_watch)
                 .await
-                .map_err(|error| anyhow!("{}", error))
+                .map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
         })
     }
 

crates/project2/src/worktree.rs 🔗

@@ -3684,7 +3684,7 @@ impl BackgroundScanner {
                 }
                 Err(err) => {
                     // TODO - create a special 'error' entry in the entries tree to mark this
-                    log::error!("error reading file on event {:?}", err);
+                    log::error!("error reading file {abs_path:?} on event: {err:#}");
                 }
             }
         }

crates/workspace2/src/modal_layer.rs 🔗

@@ -72,7 +72,7 @@ impl ModalLayer {
         cx.notify();
     }
 
-    pub fn current_modal<V>(&self) -> Option<View<V>>
+    pub fn active_modal<V>(&self) -> Option<View<V>>
     where
         V: 'static,
     {

crates/workspace2/src/pane.rs 🔗

@@ -8,8 +8,8 @@ use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
     actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
-    EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext,
-    WeakView, WindowContext,
+    EventEmitter, FocusHandle, Focusable, Model, PromptLevel, Render, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project2::{Project, ProjectEntryId, ProjectPath};
@@ -1017,7 +1017,11 @@ impl Pane {
                 .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
 
             let should_activate = activate_pane || self.has_focus(cx);
-            self.activate_item(index_to_activate, should_activate, should_activate, cx);
+            if self.items.len() == 1 && should_activate {
+                self.focus_handle.focus(cx);
+            } else {
+                self.activate_item(index_to_activate, should_activate, should_activate, cx);
+            }
         }
 
         let item = self.items.remove(item_index);
@@ -1913,11 +1917,12 @@ impl Pane {
 // }
 
 impl Render for Pane {
-    type Element = Div<Self>;
+    type Element = Focusable<Self, Div<Self>>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         v_stack()
             .key_context("Pane")
+            .track_focus(&self.focus_handle)
             .size_full()
             .on_action(|pane: &mut Self, action, cx| {
                 pane.close_active_item(action, cx)

crates/workspace2/src/workspace2.rs 🔗

@@ -38,9 +38,9 @@ use futures::{
 use gpui::{
     actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
     AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
-    EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent,
-    Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds,
-    WindowContext, WindowHandle, WindowOptions,
+    EventEmitter, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, Point, Render,
+    Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, WindowContext,
+    WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -433,7 +433,6 @@ pub enum Event {
 
 pub struct Workspace {
     weak_self: WeakView<Self>,
-    focus_handle: FocusHandle,
     workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
     zoomed: Option<AnyWeakView>,
     zoomed_position: Option<DockPosition>,
@@ -651,7 +650,6 @@ impl Workspace {
         cx.defer(|this, cx| this.update_window_title(cx));
         Workspace {
             weak_self: weak_handle.clone(),
-            focus_handle: cx.focus_handle(),
             zoomed: None,
             zoomed_position: None,
             center: PaneGroup::new(center_pane.clone()),
@@ -1450,6 +1448,11 @@ impl Workspace {
         self.active_pane().read(cx).active_item()
     }
 
+    pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
+        let item = self.active_item(cx)?;
+        item.to_any().downcast::<I>().ok()
+    }
+
     fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
         self.active_item(cx).and_then(|item| item.project_path(cx))
     }
@@ -1570,7 +1573,7 @@ impl Workspace {
         }
 
         if focus_center {
-            cx.focus(&self.focus_handle);
+            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
         }
 
         cx.notify();
@@ -1704,7 +1707,7 @@ impl Workspace {
         }
 
         if focus_center {
-            cx.focus(&self.focus_handle);
+            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
         }
 
         if self.zoomed_position != dock_to_reveal {
@@ -3475,8 +3478,8 @@ 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 active_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
+        self.modal_layer.read(cx).active_modal()
     }
 
     pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.113.0"
+version = "0.114.0"
 publish = false
 
 [lib]