Merge pull request #380 from zed-industries/find-bar

Antonio Scandurra created

Find in buffer

Change summary

Cargo.lock                                  |  18 
crates/chat_panel/src/chat_panel.rs         |   4 
crates/diagnostics/src/diagnostics.rs       |   2 
crates/editor/src/editor.rs                 | 197 ++++
crates/editor/src/element.rs                | 210 +++-
crates/editor/src/items.rs                  |   2 
crates/editor/src/movement.rs               |  22 
crates/editor/src/multi_buffer.rs           |  59 +
crates/file_finder/src/file_finder.rs       |   2 
crates/find/Cargo.toml                      |  25 
crates/find/src/find.rs                     | 945 +++++++++++++++++++++++
crates/go_to_line/src/go_to_line.rs         |   2 
crates/gpui/src/app.rs                      |  16 
crates/gpui/src/presenter.rs                |  10 
crates/outline/src/outline.rs               |   2 
crates/theme/src/theme.rs                   |  29 
crates/theme_selector/src/theme_selector.rs |   2 
crates/workspace/src/pane.rs                | 141 +++
crates/workspace/src/pane_group.rs          |  65 
crates/workspace/src/sidebar.rs             |   2 
crates/workspace/src/status_bar.rs          |  20 
crates/workspace/src/workspace.rs           |  12 
crates/zed/Cargo.toml                       |   1 
crates/zed/assets/themes/_base.toml         |  50 +
crates/zed/src/main.rs                      |   1 
25 files changed, 1,660 insertions(+), 179 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1719,6 +1719,23 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "find"
+version = "0.1.0"
+dependencies = [
+ "aho-corasick",
+ "anyhow",
+ "collections",
+ "editor",
+ "gpui",
+ "postage",
+ "regex",
+ "smol",
+ "theme",
+ "unindent",
+ "workspace",
+]
+
 [[package]]
 name = "fixedbitset"
 version = "0.2.0"
@@ -5725,6 +5742,7 @@ dependencies = [
  "editor",
  "env_logger",
  "file_finder",
+ "find",
  "fsevent",
  "futures",
  "fuzzy",

crates/chat_panel/src/chat_panel.rs 🔗

@@ -217,7 +217,7 @@ impl ChatPanel {
         let theme = &self.settings.borrow().theme;
         Flex::column()
             .with_child(
-                Container::new(ChildView::new(self.channel_select.id()).boxed())
+                Container::new(ChildView::new(&self.channel_select).boxed())
                     .with_style(theme.chat_panel.channel_select.container)
                     .boxed(),
             )
@@ -282,7 +282,7 @@ impl ChatPanel {
 
     fn render_input_box(&self) -> ElementBox {
         let theme = &self.settings.borrow().theme;
-        Container::new(ChildView::new(self.input_editor.id()).boxed())
+        Container::new(ChildView::new(&self.input_editor).boxed())
             .with_style(theme.chat_panel.input_editor.container)
             .boxed()
     }

crates/diagnostics/src/diagnostics.rs 🔗

@@ -111,7 +111,7 @@ impl View for ProjectDiagnosticsEditor {
             .with_style(theme.container)
             .boxed()
         } else {
-            ChildView::new(self.editor.id()).boxed()
+            ChildView::new(&self.editor).boxed()
         }
     }
 

crates/editor/src/editor.rs 🔗

@@ -9,12 +9,13 @@ mod test;
 
 use aho_corasick::AhoCorasick;
 use clock::ReplicaId;
-use collections::{HashMap, HashSet};
+use collections::{BTreeMap, HashMap, HashSet};
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
 use gpui::{
     action,
+    color::Color,
     elements::*,
     fonts::TextStyle,
     geometry::vector::{vec2f, Vector2F},
@@ -28,23 +29,25 @@ use language::{
     AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point,
     Selection, SelectionGoal, TransactionId,
 };
+use multi_buffer::MultiBufferChunks;
 pub use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot,
+    ToOffset, ToPoint,
 };
-use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
 use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use smol::Timer;
 use std::{
-    cmp,
+    any::TypeId,
+    cmp::{self, Ordering},
     iter::{self, FromIterator},
     mem,
     ops::{Deref, Range, RangeInclusive, Sub},
     sync::Arc,
     time::{Duration, Instant},
 };
-use sum_tree::Bias;
+pub use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::{DiagnosticStyle, EditorStyle};
 use util::post_inc;
@@ -382,6 +385,7 @@ pub struct Editor {
     vertical_scroll_margin: f32,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
+    highlighted_ranges: BTreeMap<TypeId, (Color, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
 }
 
@@ -435,6 +439,14 @@ pub struct NavigationData {
     offset: usize,
 }
 
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
+pub enum CharKind {
+    Newline,
+    Punctuation,
+    Whitespace,
+    Word,
+}
+
 impl Editor {
     pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext<Self>) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
@@ -522,6 +534,7 @@ impl Editor {
             vertical_scroll_margin: 3.0,
             placeholder_text: None,
             highlighted_rows: None,
+            highlighted_ranges: Default::default(),
             nav_history: None,
         };
         let selection = Selection {
@@ -848,7 +861,7 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = &display_map.buffer_snapshot;
-        let newest_selection = self.newest_selection_internal().unwrap().clone();
+        let newest_selection = self.newest_anchor_selection().unwrap().clone();
 
         let start;
         let end;
@@ -1078,6 +1091,11 @@ impl Editor {
     }
 
     pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+        if self.mode != EditorMode::Full {
+            cx.propagate_action();
+            return;
+        }
+
         if self.active_diagnostics.is_some() {
             self.dismiss_diagnostics(cx);
         } else if let Some(PendingSelection { selection, .. }) = self.pending_selection.take() {
@@ -3345,10 +3363,10 @@ impl Editor {
         &self,
         snapshot: &MultiBufferSnapshot,
     ) -> Selection<D> {
-        self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot)
+        self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot)
     }
 
-    pub fn newest_selection_internal(&self) -> Option<&Selection<Anchor>> {
+    pub fn newest_anchor_selection(&self) -> Option<&Selection<Anchor>> {
         self.pending_selection
             .as_ref()
             .map(|s| &s.selection)
@@ -3364,7 +3382,7 @@ impl Editor {
         T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
     {
         let buffer = self.buffer.read(cx).snapshot(cx);
-        let old_cursor_position = self.newest_selection_internal().map(|s| s.head());
+        let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
         selections.sort_unstable_by_key(|s| s.start);
 
         // Merge overlapping selections.
@@ -3498,6 +3516,7 @@ impl Editor {
                 buffer.set_active_selections(&self.selections, cx)
             });
         }
+        cx.emit(Event::SelectionsChanged);
     }
 
     pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
@@ -3721,6 +3740,76 @@ impl Editor {
         self.highlighted_rows.clone()
     }
 
+    pub fn highlight_ranges<T: 'static>(
+        &mut self,
+        ranges: Vec<Range<Anchor>>,
+        color: Color,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.highlighted_ranges
+            .insert(TypeId::of::<T>(), (color, ranges));
+        cx.notify();
+    }
+
+    pub fn clear_highlighted_ranges<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+        self.highlighted_ranges.remove(&TypeId::of::<T>());
+        cx.notify();
+    }
+
+    #[cfg(feature = "test-support")]
+    pub fn all_highlighted_ranges(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Vec<(Range<DisplayPoint>, Color)> {
+        let snapshot = self.snapshot(cx);
+        let buffer = &snapshot.buffer_snapshot;
+        let start = buffer.anchor_before(0);
+        let end = buffer.anchor_after(buffer.len());
+        self.highlighted_ranges_in_range(start..end, &snapshot)
+    }
+
+    pub fn highlighted_ranges_for_type<T: 'static>(&self) -> Option<(Color, &[Range<Anchor>])> {
+        self.highlighted_ranges
+            .get(&TypeId::of::<T>())
+            .map(|(color, ranges)| (*color, ranges.as_slice()))
+    }
+
+    pub fn highlighted_ranges_in_range(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+    ) -> Vec<(Range<DisplayPoint>, Color)> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        for (color, ranges) in self.highlighted_ranges.values() {
+            let start_ix = match ranges.binary_search_by(|probe| {
+                let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap();
+                if cmp.is_gt() {
+                    Ordering::Greater
+                } else {
+                    Ordering::Less
+                }
+            }) {
+                Ok(i) | Err(i) => i,
+            };
+            for range in &ranges[start_ix..] {
+                if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() {
+                    break;
+                }
+                let start = range
+                    .start
+                    .to_point(buffer)
+                    .to_display_point(display_snapshot);
+                let end = range
+                    .end
+                    .to_point(buffer)
+                    .to_display_point(display_snapshot);
+                results.push((start..end, *color))
+            }
+        }
+        results
+    }
+
     fn next_blink_epoch(&mut self) -> usize {
         self.blink_epoch += 1;
         self.blink_epoch
@@ -3934,6 +4023,7 @@ pub enum Event {
     Dirtied,
     Saved,
     TitleChanged,
+    SelectionsChanged,
     Closed,
 }
 
@@ -4159,6 +4249,18 @@ pub fn settings_builder(
     })
 }
 
+pub fn char_kind(c: char) -> CharKind {
+    if c == '\n' {
+        CharKind::Newline
+    } else if c.is_whitespace() {
+        CharKind::Whitespace
+    } else if c.is_alphanumeric() || c == '_' {
+        CharKind::Word
+    } else {
+        CharKind::Punctuation
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -6555,6 +6657,83 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
+        let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+        let settings = EditorSettings::test(&cx);
+        let (_, editor) = cx.add_window(Default::default(), |cx| {
+            build_editor(buffer.clone(), settings, cx)
+        });
+
+        editor.update(cx, |editor, cx| {
+            struct Type1;
+            struct Type2;
+
+            let buffer = buffer.read(cx).snapshot(cx);
+
+            let anchor_range = |range: Range<Point>| {
+                buffer.anchor_after(range.start)..buffer.anchor_after(range.end)
+            };
+
+            editor.highlight_ranges::<Type1>(
+                vec![
+                    anchor_range(Point::new(2, 1)..Point::new(2, 3)),
+                    anchor_range(Point::new(4, 2)..Point::new(4, 4)),
+                    anchor_range(Point::new(6, 3)..Point::new(6, 5)),
+                    anchor_range(Point::new(8, 4)..Point::new(8, 6)),
+                ],
+                Color::red(),
+                cx,
+            );
+            editor.highlight_ranges::<Type2>(
+                vec![
+                    anchor_range(Point::new(3, 2)..Point::new(3, 5)),
+                    anchor_range(Point::new(5, 3)..Point::new(5, 6)),
+                    anchor_range(Point::new(7, 4)..Point::new(7, 7)),
+                    anchor_range(Point::new(9, 5)..Point::new(9, 8)),
+                ],
+                Color::green(),
+                cx,
+            );
+
+            let snapshot = editor.snapshot(cx);
+            assert_eq!(
+                editor.highlighted_ranges_in_range(
+                    anchor_range(Point::new(3, 4)..Point::new(7, 4)),
+                    &snapshot,
+                ),
+                &[
+                    (
+                        DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
+                        Color::green(),
+                    ),
+                    (
+                        DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
+                        Color::green(),
+                    ),
+                ]
+            );
+            assert_eq!(
+                editor.highlighted_ranges_in_range(
+                    anchor_range(Point::new(5, 6)..Point::new(6, 4)),
+                    &snapshot,
+                ),
+                &[(
+                    DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+                    Color::red(),
+                )]
+            );
+        });
+    }
+
     fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
         let point = DisplayPoint::new(row as u32, column as u32);
         point..point

crates/editor/src/element.rs 🔗

@@ -312,64 +312,47 @@ impl EditorElement {
         let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
         let max_glyph_width = layout.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
+        let content_origin = bounds.origin() + layout.text_offset;
 
         cx.scene.push_layer(Some(bounds));
 
-        // Draw selections
-        let corner_radius = 2.5;
-        let mut cursors = SmallVec::<[Cursor; 32]>::new();
-
-        let content_origin = bounds.origin() + layout.text_offset;
+        for (range, color) in &layout.highlighted_ranges {
+            self.paint_highlighted_range(
+                range.clone(),
+                start_row,
+                end_row,
+                *color,
+                0.,
+                0.15 * layout.line_height,
+                layout,
+                content_origin,
+                scroll_top,
+                scroll_left,
+                bounds,
+                cx,
+            );
+        }
 
+        let mut cursors = SmallVec::<[Cursor; 32]>::new();
         for (replica_id, selections) in &layout.selections {
             let style = style.replica_selection_style(*replica_id);
+            let corner_radius = 0.15 * layout.line_height;
 
             for selection in selections {
-                if selection.start != selection.end {
-                    let row_range = if selection.end.column() == 0 {
-                        cmp::max(selection.start.row(), start_row)
-                            ..cmp::min(selection.end.row(), end_row)
-                    } else {
-                        cmp::max(selection.start.row(), start_row)
-                            ..cmp::min(selection.end.row() + 1, end_row)
-                    };
-
-                    let selection = Selection {
-                        color: style.selection,
-                        line_height: layout.line_height,
-                        start_y: content_origin.y() + row_range.start as f32 * layout.line_height
-                            - scroll_top,
-                        lines: row_range
-                            .into_iter()
-                            .map(|row| {
-                                let line_layout = &layout.line_layouts[(row - start_row) as usize];
-                                SelectionLine {
-                                    start_x: if row == selection.start.row() {
-                                        content_origin.x()
-                                            + line_layout
-                                                .x_for_index(selection.start.column() as usize)
-                                            - scroll_left
-                                    } else {
-                                        content_origin.x() - scroll_left
-                                    },
-                                    end_x: if row == selection.end.row() {
-                                        content_origin.x()
-                                            + line_layout
-                                                .x_for_index(selection.end.column() as usize)
-                                            - scroll_left
-                                    } else {
-                                        content_origin.x()
-                                            + line_layout.width()
-                                            + corner_radius * 2.0
-                                            - scroll_left
-                                    },
-                                }
-                            })
-                            .collect(),
-                    };
-
-                    selection.paint(bounds, cx.scene);
-                }
+                self.paint_highlighted_range(
+                    selection.start..selection.end,
+                    start_row,
+                    end_row,
+                    style.selection,
+                    corner_radius,
+                    corner_radius * 2.,
+                    layout,
+                    content_origin,
+                    scroll_top,
+                    scroll_left,
+                    bounds,
+                    cx,
+                );
 
                 if view.show_local_cursors() || *replica_id != local_replica_id {
                     let cursor_position = selection.head();
@@ -412,6 +395,63 @@ impl EditorElement {
         cx.scene.pop_layer();
     }
 
+    fn paint_highlighted_range(
+        &self,
+        range: Range<DisplayPoint>,
+        start_row: u32,
+        end_row: u32,
+        color: Color,
+        corner_radius: f32,
+        line_end_overshoot: f32,
+        layout: &LayoutState,
+        content_origin: Vector2F,
+        scroll_top: f32,
+        scroll_left: f32,
+        bounds: RectF,
+        cx: &mut PaintContext,
+    ) {
+        if range.start != range.end {
+            let row_range = if range.end.column() == 0 {
+                cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+            } else {
+                cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+            };
+
+            let highlighted_range = HighlightedRange {
+                color,
+                line_height: layout.line_height,
+                corner_radius,
+                start_y: content_origin.y() + row_range.start as f32 * layout.line_height
+                    - scroll_top,
+                lines: row_range
+                    .into_iter()
+                    .map(|row| {
+                        let line_layout = &layout.line_layouts[(row - start_row) as usize];
+                        HighlightedRangeLine {
+                            start_x: if row == range.start.row() {
+                                content_origin.x()
+                                    + line_layout.x_for_index(range.start.column() as usize)
+                                    - scroll_left
+                            } else {
+                                content_origin.x() - scroll_left
+                            },
+                            end_x: if row == range.end.row() {
+                                content_origin.x()
+                                    + line_layout.x_for_index(range.end.column() as usize)
+                                    - scroll_left
+                            } else {
+                                content_origin.x() + line_layout.width() + line_end_overshoot
+                                    - scroll_left
+                            },
+                        }
+                    })
+                    .collect(),
+            };
+
+            highlighted_range.paint(bounds, cx.scene);
+        }
+    }
+
     fn paint_blocks(
         &mut self,
         bounds: RectF,
@@ -715,10 +755,16 @@ impl Element for EditorElement {
         let mut selections = HashMap::default();
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
+        let mut highlighted_ranges = Vec::new();
         self.update_view(cx.app, |view, cx| {
-            highlighted_rows = view.highlighted_rows();
             let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
 
+            highlighted_rows = view.highlighted_rows();
+            highlighted_ranges = view.highlighted_ranges_in_range(
+                start_anchor.clone()..end_anchor.clone(),
+                &display_map,
+            );
+
             let local_selections = view
                 .local_selections_in_range(start_anchor.clone()..end_anchor.clone(), &display_map);
             for selection in &local_selections {
@@ -837,6 +883,7 @@ impl Element for EditorElement {
                 snapshot,
                 active_rows,
                 highlighted_rows,
+                highlighted_ranges,
                 line_layouts,
                 line_number_layouts,
                 blocks,
@@ -950,6 +997,7 @@ pub struct LayoutState {
     line_height: f32,
     em_width: f32,
     em_advance: f32,
+    highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
     text_offset: Vector2F,
 }
@@ -1036,20 +1084,21 @@ impl Cursor {
 }
 
 #[derive(Debug)]
-struct Selection {
+struct HighlightedRange {
     start_y: f32,
     line_height: f32,
-    lines: Vec<SelectionLine>,
+    lines: Vec<HighlightedRangeLine>,
     color: Color,
+    corner_radius: f32,
 }
 
 #[derive(Debug)]
-struct SelectionLine {
+struct HighlightedRangeLine {
     start_x: f32,
     end_x: f32,
 }
 
-impl Selection {
+impl HighlightedRange {
     fn paint(&self, bounds: RectF, scene: &mut Scene) {
         if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
             self.paint_lines(self.start_y, &self.lines[0..1], bounds, scene);
@@ -1064,26 +1113,31 @@ impl Selection {
         }
     }
 
-    fn paint_lines(&self, start_y: f32, lines: &[SelectionLine], bounds: RectF, scene: &mut Scene) {
+    fn paint_lines(
+        &self,
+        start_y: f32,
+        lines: &[HighlightedRangeLine],
+        bounds: RectF,
+        scene: &mut Scene,
+    ) {
         if lines.is_empty() {
             return;
         }
 
         let mut path = PathBuilder::new();
-        let corner_radius = 0.15 * self.line_height;
         let first_line = lines.first().unwrap();
         let last_line = lines.last().unwrap();
 
         let first_top_left = vec2f(first_line.start_x, start_y);
         let first_top_right = vec2f(first_line.end_x, start_y);
 
-        let curve_height = vec2f(0., corner_radius);
+        let curve_height = vec2f(0., self.corner_radius);
         let curve_width = |start_x: f32, end_x: f32| {
             let max = (end_x - start_x) / 2.;
-            let width = if max < corner_radius {
+            let width = if max < self.corner_radius {
                 max
             } else {
-                corner_radius
+                self.corner_radius
             };
 
             vec2f(width, 0.)
@@ -1107,26 +1161,38 @@ impl Selection {
                     Ordering::Less => {
                         let curve_width = curve_width(next_top_right.x(), bottom_right.x());
                         path.line_to(bottom_right - curve_height);
-                        path.curve_to(bottom_right - curve_width, bottom_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(bottom_right - curve_width, bottom_right);
+                        }
                         path.line_to(next_top_right + curve_width);
-                        path.curve_to(next_top_right + curve_height, next_top_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(next_top_right + curve_height, next_top_right);
+                        }
                     }
                     Ordering::Greater => {
                         let curve_width = curve_width(bottom_right.x(), next_top_right.x());
                         path.line_to(bottom_right - curve_height);
-                        path.curve_to(bottom_right + curve_width, bottom_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(bottom_right + curve_width, bottom_right);
+                        }
                         path.line_to(next_top_right - curve_width);
-                        path.curve_to(next_top_right + curve_height, next_top_right);
+                        if self.corner_radius > 0. {
+                            path.curve_to(next_top_right + curve_height, next_top_right);
+                        }
                     }
                 }
             } else {
                 let curve_width = curve_width(line.start_x, line.end_x);
                 path.line_to(bottom_right - curve_height);
-                path.curve_to(bottom_right - curve_width, bottom_right);
+                if self.corner_radius > 0. {
+                    path.curve_to(bottom_right - curve_width, bottom_right);
+                }
 
                 let bottom_left = vec2f(line.start_x, bottom_right.y());
                 path.line_to(bottom_left + curve_width);
-                path.curve_to(bottom_left - curve_height, bottom_left);
+                if self.corner_radius > 0. {
+                    path.curve_to(bottom_left - curve_height, bottom_left);
+                }
             }
         }
 
@@ -1134,14 +1200,20 @@ impl Selection {
             let curve_width = curve_width(last_line.start_x, first_line.start_x);
             let second_top_left = vec2f(last_line.start_x, start_y + self.line_height);
             path.line_to(second_top_left + curve_height);
-            path.curve_to(second_top_left + curve_width, second_top_left);
+            if self.corner_radius > 0. {
+                path.curve_to(second_top_left + curve_width, second_top_left);
+            }
             let first_bottom_left = vec2f(first_line.start_x, second_top_left.y());
             path.line_to(first_bottom_left - curve_width);
-            path.curve_to(first_bottom_left - curve_height, first_bottom_left);
+            if self.corner_radius > 0. {
+                path.curve_to(first_bottom_left - curve_height, first_bottom_left);
+            }
         }
 
         path.line_to(first_top_left + curve_height);
-        path.curve_to(first_top_left + top_curve_width, first_top_left);
+        if self.corner_radius > 0. {
+            path.curve_to(first_top_left + top_curve_width, first_top_left);
+        }
         path.line_to(first_top_right - top_curve_width);
 
         scene.push_path(path.build(self.color, Some(bounds)));

crates/editor/src/items.rs 🔗

@@ -141,7 +141,7 @@ impl ItemView for Editor {
     }
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(selection) = self.newest_selection_internal() {
+        if let Some(selection) = self.newest_anchor_selection() {
             self.push_to_nav_history(selection.head(), None, cx);
         }
     }

crates/editor/src/movement.rs 🔗

@@ -1,5 +1,5 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
-use crate::ToPoint;
+use crate::{char_kind, CharKind, ToPoint};
 use anyhow::Result;
 use std::{cmp, ops::Range};
 
@@ -215,26 +215,6 @@ pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<Dis
         ..end.to_point(&map.buffer_snapshot).to_display_point(map)
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
-enum CharKind {
-    Newline,
-    Punctuation,
-    Whitespace,
-    Word,
-}
-
-fn char_kind(c: char) -> CharKind {
-    if c == '\n' {
-        CharKind::Newline
-    } else if c.is_whitespace() {
-        CharKind::Whitespace
-    } else if c.is_alphanumeric() || c == '_' {
-        CharKind::Word
-    } else {
-        CharKind::Punctuation
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/editor/src/multi_buffer.rs 🔗

@@ -76,6 +76,7 @@ struct BufferState {
 
 #[derive(Clone, Default)]
 pub struct MultiBufferSnapshot {
+    singleton: bool,
     excerpts: SumTree<Excerpt>,
     parse_count: usize,
     diagnostics_update_count: usize,
@@ -163,6 +164,7 @@ impl MultiBuffer {
             },
             cx,
         );
+        this.snapshot.borrow_mut().singleton = true;
         this
     }
 
@@ -1079,11 +1081,9 @@ impl MultiBufferSnapshot {
                 .eq(needle.bytes())
     }
 
-    fn as_singleton(&self) -> Option<&BufferSnapshot> {
-        let mut excerpts = self.excerpts.iter();
-        let buffer = excerpts.next().map(|excerpt| &excerpt.buffer);
-        if excerpts.next().is_none() {
-            buffer
+    fn as_singleton(&self) -> Option<&Excerpt> {
+        if self.singleton {
+            self.excerpts.iter().next()
         } else {
             None
         }
@@ -1098,6 +1098,10 @@ impl MultiBufferSnapshot {
     }
 
     pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
+        if let Some(excerpt) = self.as_singleton() {
+            return excerpt.buffer.clip_offset(offset, bias);
+        }
+
         let mut cursor = self.excerpts.cursor::<usize>();
         cursor.seek(&offset, Bias::Right, &());
         let overshoot = if let Some(excerpt) = cursor.item() {
@@ -1113,6 +1117,10 @@ impl MultiBufferSnapshot {
     }
 
     pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
+        if let Some(excerpt) = self.as_singleton() {
+            return excerpt.buffer.clip_point(point, bias);
+        }
+
         let mut cursor = self.excerpts.cursor::<Point>();
         cursor.seek(&point, Bias::Right, &());
         let overshoot = if let Some(excerpt) = cursor.item() {
@@ -1128,6 +1136,10 @@ impl MultiBufferSnapshot {
     }
 
     pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 {
+        if let Some(excerpt) = self.as_singleton() {
+            return excerpt.buffer.clip_point_utf16(point, bias);
+        }
+
         let mut cursor = self.excerpts.cursor::<PointUtf16>();
         cursor.seek(&point, Bias::Right, &());
         let overshoot = if let Some(excerpt) = cursor.item() {
@@ -1193,6 +1205,10 @@ impl MultiBufferSnapshot {
     }
 
     pub fn offset_to_point(&self, offset: usize) -> Point {
+        if let Some(excerpt) = self.as_singleton() {
+            return excerpt.buffer.offset_to_point(offset);
+        }
+
         let mut cursor = self.excerpts.cursor::<(usize, Point)>();
         cursor.seek(&offset, Bias::Right, &());
         if let Some(excerpt) = cursor.item() {
@@ -1210,6 +1226,10 @@ impl MultiBufferSnapshot {
     }
 
     pub fn point_to_offset(&self, point: Point) -> usize {
+        if let Some(excerpt) = self.as_singleton() {
+            return excerpt.buffer.point_to_offset(point);
+        }
+
         let mut cursor = self.excerpts.cursor::<(Point, usize)>();
         cursor.seek(&point, Bias::Right, &());
         if let Some(excerpt) = cursor.item() {
@@ -1227,6 +1247,10 @@ impl MultiBufferSnapshot {
     }
 
     pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize {
+        if let Some(excerpt) = self.as_singleton() {
+            return excerpt.buffer.point_utf16_to_offset(point);
+        }
+
         let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>();
         cursor.seek(&point, Bias::Right, &());
         if let Some(excerpt) = cursor.item() {
@@ -1520,6 +1544,14 @@ impl MultiBufferSnapshot {
 
     pub fn anchor_at<T: ToOffset>(&self, position: T, mut bias: Bias) -> Anchor {
         let offset = position.to_offset(self);
+        if let Some(excerpt) = self.as_singleton() {
+            return Anchor {
+                buffer_id: excerpt.buffer_id,
+                excerpt_id: excerpt.id.clone(),
+                text_anchor: excerpt.buffer.anchor_at(offset, bias),
+            };
+        }
+
         let mut cursor = self.excerpts.cursor::<(usize, Option<&ExcerptId>)>();
         cursor.seek(&offset, Bias::Right, &());
         if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left {
@@ -1675,7 +1707,7 @@ impl MultiBufferSnapshot {
     {
         self.as_singleton()
             .into_iter()
-            .flat_map(move |buffer| buffer.diagnostic_group(group_id))
+            .flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id))
     }
 
     pub fn diagnostics_in_range<'a, T, O>(
@@ -1686,8 +1718,10 @@ impl MultiBufferSnapshot {
         T: 'a + ToOffset,
         O: 'a + text::FromAnchor,
     {
-        self.as_singleton().into_iter().flat_map(move |buffer| {
-            buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self))
+        self.as_singleton().into_iter().flat_map(move |excerpt| {
+            excerpt
+                .buffer
+                .diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self))
         })
     }
 
@@ -1730,17 +1764,16 @@ impl MultiBufferSnapshot {
     }
 
     pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
-        let buffer = self.as_singleton()?;
-        let outline = buffer.outline(theme)?;
-        let excerpt_id = &self.excerpts.iter().next().unwrap().id;
+        let excerpt = self.as_singleton()?;
+        let outline = excerpt.buffer.outline(theme)?;
         Some(Outline::new(
             outline
                 .items
                 .into_iter()
                 .map(|item| OutlineItem {
                     depth: item.depth,
-                    range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
-                        ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
+                    range: self.anchor_in_excerpt(excerpt.id.clone(), item.range.start)
+                        ..self.anchor_in_excerpt(excerpt.id.clone(), item.range.end),
                     text: item.text,
                     highlight_ranges: item.highlight_ranges,
                     name_ranges: item.name_ranges,

crates/file_finder/src/file_finder.rs 🔗

@@ -76,7 +76,7 @@ impl View for FileFinder {
                 Container::new(
                     Flex::new(Axis::Vertical)
                         .with_child(
-                            Container::new(ChildView::new(self.query_editor.id()).boxed())
+                            Container::new(ChildView::new(&self.query_editor).boxed())
                                 .with_style(settings.theme.selector.input_editor.container)
                                 .boxed(),
                         )

crates/find/Cargo.toml 🔗

@@ -0,0 +1,25 @@
+[package]
+name = "find"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/find.rs"
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+aho-corasick = "0.7"
+anyhow = "1.0"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+regex = "1.5"
+smol = { version = "1.2" }
+
+[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+unindent = "0.1"

crates/find/src/find.rs 🔗

@@ -0,0 +1,945 @@
+use aho_corasick::AhoCorasickBuilder;
+use anyhow::Result;
+use collections::HashSet;
+use editor::{
+    char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings,
+    MultiBufferSnapshot,
+};
+use gpui::{
+    action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
+    RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use postage::watch;
+use regex::RegexBuilder;
+use smol::future::yield_now;
+use std::{
+    cmp::{self, Ordering},
+    ops::Range,
+    sync::Arc,
+};
+use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace};
+
+action!(Deploy, bool);
+action!(Dismiss);
+action!(FocusEditor);
+action!(ToggleMode, SearchMode);
+action!(GoToMatch, Direction);
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum Direction {
+    Prev,
+    Next,
+}
+
+#[derive(Clone, Copy)]
+pub enum SearchMode {
+    WholeWord,
+    CaseSensitive,
+    Regex,
+}
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_bindings([
+        Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
+        Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
+        Binding::new("escape", Dismiss, Some("FindBar")),
+        Binding::new("cmd-f", FocusEditor, Some("FindBar")),
+        Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")),
+        Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")),
+        Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")),
+        Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")),
+    ]);
+    cx.add_action(FindBar::deploy);
+    cx.add_action(FindBar::dismiss);
+    cx.add_action(FindBar::focus_editor);
+    cx.add_action(FindBar::toggle_mode);
+    cx.add_action(FindBar::go_to_match);
+    cx.add_action(FindBar::go_to_match_on_pane);
+}
+
+struct FindBar {
+    settings: watch::Receiver<Settings>,
+    query_editor: ViewHandle<Editor>,
+    active_editor: Option<ViewHandle<Editor>>,
+    active_match_index: Option<usize>,
+    active_editor_subscription: Option<Subscription>,
+    highlighted_editors: HashSet<WeakViewHandle<Editor>>,
+    pending_search: Option<Task<()>>,
+    case_sensitive_mode: bool,
+    whole_word_mode: bool,
+    regex_mode: bool,
+    query_contains_error: bool,
+}
+
+impl Entity for FindBar {
+    type Event = ();
+}
+
+impl View for FindBar {
+    fn ui_name() -> &'static str {
+        "FindBar"
+    }
+
+    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
+        cx.focus(&self.query_editor);
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &self.settings.borrow().theme;
+        let editor_container = if self.query_contains_error {
+            theme.find.invalid_editor
+        } else {
+            theme.find.editor.input.container
+        };
+        Flex::row()
+            .with_child(
+                ChildView::new(&self.query_editor)
+                    .contained()
+                    .with_style(editor_container)
+                    .aligned()
+                    .constrained()
+                    .with_max_width(theme.find.editor.max_width)
+                    .boxed(),
+            )
+            .with_child(
+                Flex::row()
+                    .with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx))
+                    .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx))
+                    .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx))
+                    .contained()
+                    .with_style(theme.find.mode_button_group)
+                    .aligned()
+                    .boxed(),
+            )
+            .with_child(
+                Flex::row()
+                    .with_child(self.render_nav_button("<", Direction::Prev, cx))
+                    .with_child(self.render_nav_button(">", Direction::Next, cx))
+                    .aligned()
+                    .boxed(),
+            )
+            .with_children(self.active_editor.as_ref().and_then(|editor| {
+                let (_, highlighted_ranges) =
+                    editor.read(cx).highlighted_ranges_for_type::<Self>()?;
+                let message = if let Some(match_ix) = self.active_match_index {
+                    format!("{}/{}", match_ix + 1, highlighted_ranges.len())
+                } else {
+                    "No matches".to_string()
+                };
+
+                Some(
+                    Label::new(message, theme.find.match_index.text.clone())
+                        .contained()
+                        .with_style(theme.find.match_index.container)
+                        .aligned()
+                        .boxed(),
+                )
+            }))
+            .contained()
+            .with_style(theme.find.container)
+            .constrained()
+            .with_height(theme.workspace.toolbar.height)
+            .named("find bar")
+    }
+}
+
+impl Toolbar for FindBar {
+    fn active_item_changed(
+        &mut self,
+        item: Option<Box<dyn ItemViewHandle>>,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        self.active_editor_subscription.take();
+        self.active_editor.take();
+        self.pending_search.take();
+
+        if let Some(editor) = item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self.active_editor_subscription =
+                Some(cx.subscribe(&editor, Self::on_active_editor_event));
+            self.active_editor = Some(editor);
+            self.update_matches(cx);
+            true
+        } else {
+            false
+        }
+    }
+
+    fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
+        self.active_editor.take();
+        self.active_editor_subscription.take();
+        self.active_match_index.take();
+        self.pending_search.take();
+        self.clear_matches(cx);
+    }
+}
+
+impl FindBar {
+    fn new(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
+        let query_editor = cx.add_view(|cx| {
+            Editor::auto_height(
+                2,
+                {
+                    let settings = settings.clone();
+                    Arc::new(move |_| {
+                        let settings = settings.borrow();
+                        EditorSettings {
+                            style: settings.theme.find.editor.input.as_editor(),
+                            tab_size: settings.tab_size,
+                            soft_wrap: editor::SoftWrap::None,
+                        }
+                    })
+                },
+                cx,
+            )
+        });
+        cx.subscribe(&query_editor, Self::on_query_editor_event)
+            .detach();
+
+        Self {
+            query_editor,
+            active_editor: None,
+            active_editor_subscription: None,
+            active_match_index: None,
+            highlighted_editors: Default::default(),
+            case_sensitive_mode: false,
+            whole_word_mode: false,
+            regex_mode: false,
+            settings,
+            pending_search: None,
+            query_contains_error: false,
+        }
+    }
+
+    fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
+        self.query_editor.update(cx, |query_editor, cx| {
+            query_editor.buffer().update(cx, |query_buffer, cx| {
+                let len = query_buffer.read(cx).len();
+                query_buffer.edit([0..len], query, cx);
+            });
+        });
+    }
+
+    fn render_mode_button(
+        &self,
+        icon: &str,
+        mode: SearchMode,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        let theme = &self.settings.borrow().theme.find;
+        let is_active = self.is_mode_enabled(mode);
+        MouseEventHandler::new::<Self, _, _, _>((cx.view_id(), mode as usize), cx, |state, _| {
+            let style = match (is_active, state.hovered) {
+                (false, false) => &theme.mode_button,
+                (false, true) => &theme.hovered_mode_button,
+                (true, false) => &theme.active_mode_button,
+                (true, true) => &theme.active_hovered_mode_button,
+            };
+            Label::new(icon.to_string(), style.text.clone())
+                .contained()
+                .with_style(style.container)
+                .boxed()
+        })
+        .on_click(move |cx| cx.dispatch_action(ToggleMode(mode)))
+        .with_cursor_style(CursorStyle::PointingHand)
+        .boxed()
+    }
+
+    fn render_nav_button(
+        &self,
+        icon: &str,
+        direction: Direction,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        let theme = &self.settings.borrow().theme.find;
+        MouseEventHandler::new::<Self, _, _, _>(
+            (cx.view_id(), 10 + direction as usize),
+            cx,
+            |state, _| {
+                let style = if state.hovered {
+                    &theme.hovered_mode_button
+                } else {
+                    &theme.mode_button
+                };
+                Label::new(icon.to_string(), style.text.clone())
+                    .contained()
+                    .with_style(style.container)
+                    .boxed()
+            },
+        )
+        .on_click(move |cx| cx.dispatch_action(GoToMatch(direction)))
+        .with_cursor_style(CursorStyle::PointingHand)
+        .boxed()
+    }
+
+    fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
+        let settings = workspace.settings();
+        workspace.active_pane().update(cx, |pane, cx| {
+            let findbar_was_visible = pane
+                .active_toolbar()
+                .map_or(false, |toolbar| toolbar.downcast::<Self>().is_some());
+
+            pane.show_toolbar(cx, |cx| FindBar::new(settings, cx));
+
+            if let Some(find_bar) = pane
+                .active_toolbar()
+                .and_then(|toolbar| toolbar.downcast::<Self>())
+            {
+                if !findbar_was_visible {
+                    let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
+                    let display_map = editor
+                        .update(cx, |editor, cx| editor.snapshot(cx))
+                        .display_snapshot;
+                    let selection = editor
+                        .read(cx)
+                        .newest_selection::<usize>(&display_map.buffer_snapshot);
+
+                    let mut text: String;
+                    if selection.start == selection.end {
+                        let point = selection.start.to_display_point(&display_map);
+                        let range = editor::movement::surrounding_word(&display_map, point);
+                        let range = range.start.to_offset(&display_map, Bias::Left)
+                            ..range.end.to_offset(&display_map, Bias::Right);
+                        text = display_map.buffer_snapshot.text_for_range(range).collect();
+                        if text.trim().is_empty() {
+                            text = String::new();
+                        }
+                    } else {
+                        text = display_map
+                            .buffer_snapshot
+                            .text_for_range(selection.start..selection.end)
+                            .collect();
+                    }
+
+                    if !text.is_empty() {
+                        find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx));
+                    }
+                }
+
+                if *focus {
+                    if !findbar_was_visible {
+                        let query_editor = find_bar.read(cx).query_editor.clone();
+                        query_editor.update(cx, |query_editor, cx| {
+                            query_editor.select_all(&editor::SelectAll, cx);
+                        });
+                    }
+                    cx.focus(&find_bar);
+                }
+            }
+        });
+    }
+
+    fn dismiss(workspace: &mut Workspace, _: &Dismiss, cx: &mut ViewContext<Workspace>) {
+        workspace
+            .active_pane()
+            .update(cx, |pane, cx| pane.dismiss_toolbar(cx));
+    }
+
+    fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
+        if let Some(active_editor) = self.active_editor.as_ref() {
+            cx.focus(active_editor);
+        }
+    }
+
+    fn is_mode_enabled(&self, mode: SearchMode) -> bool {
+        match mode {
+            SearchMode::WholeWord => self.whole_word_mode,
+            SearchMode::CaseSensitive => self.case_sensitive_mode,
+            SearchMode::Regex => self.regex_mode,
+        }
+    }
+
+    fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext<Self>) {
+        let value = match mode {
+            SearchMode::WholeWord => &mut self.whole_word_mode,
+            SearchMode::CaseSensitive => &mut self.case_sensitive_mode,
+            SearchMode::Regex => &mut self.regex_mode,
+        };
+        *value = !*value;
+        self.update_matches(cx);
+        cx.notify();
+    }
+
+    fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext<Self>) {
+        if let Some(mut index) = self.active_match_index {
+            if let Some(editor) = self.active_editor.as_ref() {
+                editor.update(cx, |editor, cx| {
+                    let newest_selection = editor.newest_anchor_selection().cloned();
+                    if let Some(((_, ranges), newest_selection)) = editor
+                        .highlighted_ranges_for_type::<Self>()
+                        .zip(newest_selection)
+                    {
+                        let position = newest_selection.head();
+                        let buffer = editor.buffer().read(cx).read(cx);
+                        if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() {
+                            if *direction == Direction::Prev {
+                                if index == 0 {
+                                    index = ranges.len() - 1;
+                                } else {
+                                    index -= 1;
+                                }
+                            }
+                        } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() {
+                            if *direction == Direction::Next {
+                                index = 0;
+                            }
+                        } else if *direction == Direction::Prev {
+                            if index == 0 {
+                                index = ranges.len() - 1;
+                            } else {
+                                index -= 1;
+                            }
+                        } else if *direction == Direction::Next {
+                            if index == ranges.len() - 1 {
+                                index = 0
+                            } else {
+                                index += 1;
+                            }
+                        }
+
+                        let range_to_select = ranges[index].clone();
+                        drop(buffer);
+                        editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
+                    }
+                });
+            }
+        }
+    }
+
+    fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext<Pane>) {
+        if let Some(find_bar) = pane.toolbar::<FindBar>() {
+            find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx));
+        }
+    }
+
+    fn on_query_editor_event(
+        &mut self,
+        _: ViewHandle<Editor>,
+        event: &editor::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            editor::Event::Edited => {
+                self.query_contains_error = false;
+                self.clear_matches(cx);
+                self.update_matches(cx);
+                cx.notify();
+            }
+            _ => {}
+        }
+    }
+
+    fn on_active_editor_event(
+        &mut self,
+        _: ViewHandle<Editor>,
+        event: &editor::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            editor::Event::Edited => self.update_matches(cx),
+            editor::Event::SelectionsChanged => self.update_match_index(cx),
+            _ => {}
+        }
+    }
+
+    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+        for editor in self.highlighted_editors.drain() {
+            if let Some(editor) = editor.upgrade(cx) {
+                if Some(&editor) != self.active_editor.as_ref() {
+                    editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
+                }
+            }
+        }
+    }
+
+    fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+        let query = self.query_editor.read(cx).text(cx);
+        self.pending_search.take();
+        if let Some(editor) = self.active_editor.as_ref() {
+            if query.is_empty() {
+                self.active_match_index.take();
+                editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::<Self>(cx));
+            } else {
+                let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
+                let case_sensitive = self.case_sensitive_mode;
+                let whole_word = self.whole_word_mode;
+                let ranges = if self.regex_mode {
+                    cx.background()
+                        .spawn(regex_search(buffer, query, case_sensitive, whole_word))
+                } else {
+                    cx.background().spawn(async move {
+                        Ok(search(buffer, query, case_sensitive, whole_word).await)
+                    })
+                };
+
+                let editor = editor.downgrade();
+                self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+                    match ranges.await {
+                        Ok(ranges) => {
+                            if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) {
+                                this.update(&mut cx, |this, cx| {
+                                    this.highlighted_editors.insert(editor.downgrade());
+                                    editor.update(cx, |editor, cx| {
+                                        let theme = &this.settings.borrow().theme.find;
+                                        editor.highlight_ranges::<Self>(
+                                            ranges,
+                                            theme.match_background,
+                                            cx,
+                                        )
+                                    });
+                                    this.update_match_index(cx);
+                                });
+                            }
+                        }
+                        Err(_) => {
+                            this.update(&mut cx, |this, cx| {
+                                this.query_contains_error = true;
+                                cx.notify();
+                            });
+                        }
+                    }
+                }));
+            }
+        }
+    }
+
+    fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
+        self.active_match_index = self.active_match_index(cx);
+        cx.notify();
+    }
+
+    fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
+        let editor = self.active_editor.as_ref()?;
+        let editor = editor.read(cx);
+        let position = editor.newest_anchor_selection()?.head();
+        let ranges = editor.highlighted_ranges_for_type::<Self>()?.1;
+        if ranges.is_empty() {
+            None
+        } else {
+            let buffer = editor.buffer().read(cx).read(cx);
+            match ranges.binary_search_by(|probe| {
+                if probe.end.cmp(&position, &*buffer).unwrap().is_lt() {
+                    Ordering::Less
+                } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() {
+                    Ordering::Greater
+                } else {
+                    Ordering::Equal
+                }
+            }) {
+                Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
+            }
+        }
+    }
+}
+
+const YIELD_INTERVAL: usize = 20000;
+
+async fn search(
+    buffer: MultiBufferSnapshot,
+    query: String,
+    case_sensitive: bool,
+    whole_word: bool,
+) -> Vec<Range<Anchor>> {
+    let mut ranges = Vec::new();
+
+    let search = AhoCorasickBuilder::new()
+        .auto_configure(&[&query])
+        .ascii_case_insensitive(!case_sensitive)
+        .build(&[&query]);
+    for (ix, mat) in search
+        .stream_find_iter(buffer.bytes_in_range(0..buffer.len()))
+        .enumerate()
+    {
+        if (ix + 1) % YIELD_INTERVAL == 0 {
+            yield_now().await;
+        }
+
+        let mat = mat.unwrap();
+
+        if whole_word {
+            let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind);
+            let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap());
+            let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap());
+            let next_kind = buffer.chars_at(mat.end()).next().map(char_kind);
+            if Some(start_kind) == prev_kind || Some(end_kind) == next_kind {
+                continue;
+            }
+        }
+
+        ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
+    }
+
+    ranges
+}
+
+async fn regex_search(
+    buffer: MultiBufferSnapshot,
+    mut query: String,
+    case_sensitive: bool,
+    whole_word: bool,
+) -> Result<Vec<Range<Anchor>>> {
+    if whole_word {
+        let mut word_query = String::new();
+        word_query.push_str("\\b");
+        word_query.push_str(&query);
+        word_query.push_str("\\b");
+        query = word_query;
+    }
+
+    let mut ranges = Vec::new();
+
+    if query.contains("\n") || query.contains("\\n") {
+        let regex = RegexBuilder::new(&query)
+            .case_insensitive(!case_sensitive)
+            .multi_line(true)
+            .build()?;
+        for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() {
+            if (ix + 1) % YIELD_INTERVAL == 0 {
+                yield_now().await;
+            }
+
+            ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end()));
+        }
+    } else {
+        let regex = RegexBuilder::new(&query)
+            .case_insensitive(!case_sensitive)
+            .build()?;
+
+        let mut line = String::new();
+        let mut line_offset = 0;
+        for (chunk_ix, chunk) in buffer
+            .chunks(0..buffer.len(), None)
+            .map(|c| c.text)
+            .chain(["\n"])
+            .enumerate()
+        {
+            if (chunk_ix + 1) % YIELD_INTERVAL == 0 {
+                yield_now().await;
+            }
+
+            for (newline_ix, text) in chunk.split('\n').enumerate() {
+                if newline_ix > 0 {
+                    for mat in regex.find_iter(&line) {
+                        let start = line_offset + mat.start();
+                        let end = line_offset + mat.end();
+                        ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end));
+                    }
+
+                    line_offset += line.len() + 1;
+                    line.clear();
+                }
+                line.push_str(text);
+            }
+        }
+    }
+
+    Ok(ranges)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer};
+    use gpui::{color::Color, TestAppContext};
+    use std::sync::Arc;
+    use unindent::Unindent as _;
+
+    #[gpui::test]
+    async fn test_find_simple(mut cx: TestAppContext) {
+        let fonts = cx.font_cache();
+        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default());
+        theme.find.match_background = Color::red();
+        let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap();
+
+        let buffer = cx.update(|cx| {
+            MultiBuffer::build_simple(
+                &r#"
+                A regular expression (shortened as regex or regexp;[1] also referred to as
+                rational expression[2][3]) is a sequence of characters that specifies a search
+                pattern in text. Usually such patterns are used by string-searching algorithms
+                for "find" or "find and replace" operations on strings, or for input validation.
+                "#
+                .unindent(),
+                cx,
+            )
+        });
+        let editor = cx.add_view(Default::default(), |cx| {
+            Editor::new(buffer.clone(), Arc::new(EditorSettings::test), cx)
+        });
+
+        let find_bar = cx.add_view(Default::default(), |cx| {
+            let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx);
+            find_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
+            find_bar
+        });
+
+        // Search for a string that appears with different casing.
+        // By default, search is case-insensitive.
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.set_query("us", cx);
+        });
+        editor.next_notification(&cx).await;
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(
+                editor.all_highlighted_ranges(cx),
+                &[
+                    (
+                        DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
+                        Color::red(),
+                    ),
+                ]
+            );
+        });
+
+        // Switch to a case sensitive search.
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx);
+        });
+        editor.next_notification(&cx).await;
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(
+                editor.all_highlighted_ranges(cx),
+                &[(
+                    DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
+                    Color::red(),
+                )]
+            );
+        });
+
+        // Search for a string that appears both as a whole word and
+        // within other words. By default, all results are found.
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.set_query("or", cx);
+        });
+        editor.next_notification(&cx).await;
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(
+                editor.all_highlighted_ranges(cx),
+                &[
+                    (
+                        DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
+                        Color::red(),
+                    ),
+                ]
+            );
+        });
+
+        // Switch to a whole word search.
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx);
+        });
+        editor.next_notification(&cx).await;
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(
+                editor.all_highlighted_ranges(cx),
+                &[
+                    (
+                        DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
+                        Color::red(),
+                    ),
+                    (
+                        DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
+                        Color::red(),
+                    ),
+                ]
+            );
+        });
+
+        editor.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
+        });
+        find_bar.update(&mut cx, |find_bar, cx| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+        });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(1));
+        });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(2));
+        });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+        });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(2));
+        });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(1));
+        });
+
+        find_bar.update(&mut cx, |find_bar, cx| {
+            find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+        });
+
+        // Park the cursor in between matches and ensure that going to the previous match selects
+        // the closest match to the left.
+        editor.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
+        });
+        find_bar.update(&mut cx, |find_bar, cx| {
+            assert_eq!(find_bar.active_match_index, Some(1));
+            find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+        });
+
+        // Park the cursor in between matches and ensure that going to the next match selects the
+        // closest match to the right.
+        editor.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
+        });
+        find_bar.update(&mut cx, |find_bar, cx| {
+            assert_eq!(find_bar.active_match_index, Some(1));
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(1));
+        });
+
+        // Park the cursor after the last match and ensure that going to the previous match selects
+        // the last match.
+        editor.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
+        });
+        find_bar.update(&mut cx, |find_bar, cx| {
+            assert_eq!(find_bar.active_match_index, Some(2));
+            find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(2));
+        });
+
+        // Park the cursor after the last match and ensure that going to the next match selects the
+        // first match.
+        editor.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
+        });
+        find_bar.update(&mut cx, |find_bar, cx| {
+            assert_eq!(find_bar.active_match_index, Some(2));
+            find_bar.go_to_match(&GoToMatch(Direction::Next), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+        });
+
+        // Park the cursor before the first match and ensure that going to the previous match
+        // selects the last match.
+        editor.update(&mut cx, |editor, cx| {
+            editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
+        });
+        find_bar.update(&mut cx, |find_bar, cx| {
+            assert_eq!(find_bar.active_match_index, Some(0));
+            find_bar.go_to_match(&GoToMatch(Direction::Prev), cx);
+            assert_eq!(
+                editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
+                [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+            );
+        });
+        find_bar.read_with(&cx, |find_bar, _| {
+            assert_eq!(find_bar.active_match_index, Some(2));
+        });
+    }
+}

crates/go_to_line/src/go_to_line.rs 🔗

@@ -181,7 +181,7 @@ impl View for GoToLine {
                 Container::new(
                     Flex::new(Axis::Vertical)
                         .with_child(
-                            Container::new(ChildView::new(self.line_editor.id()).boxed())
+                            Container::new(ChildView::new(&self.line_editor).boxed())
                                 .with_style(theme.input_editor.container)
                                 .boxed(),
                         )

crates/gpui/src/app.rs 🔗

@@ -3246,6 +3246,7 @@ impl Drop for AnyModelHandle {
         self.ref_counts.lock().dec_model(self.model_id);
     }
 }
+
 pub struct WeakViewHandle<T> {
     window_id: usize,
     view_id: usize,
@@ -3288,6 +3289,21 @@ impl<T> Clone for WeakViewHandle<T> {
     }
 }
 
+impl<T> PartialEq for WeakViewHandle<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.window_id == other.window_id && self.view_id == other.view_id
+    }
+}
+
+impl<T> Eq for WeakViewHandle<T> {}
+
+impl<T> Hash for WeakViewHandle<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.window_id.hash(state);
+        self.view_id.hash(state);
+    }
+}
+
 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
 pub struct ElementStateId(usize, usize);
 

crates/gpui/src/presenter.rs 🔗

@@ -6,8 +6,8 @@ use crate::{
     json::{self, ToJson},
     platform::Event,
     text_layout::TextLayoutCache,
-    Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel,
-    ReadView, Scene, View, ViewHandle,
+    Action, AnyAction, AnyViewHandle, AssetCache, ElementBox, Entity, FontSystem, ModelHandle,
+    ReadModel, ReadView, Scene, View, ViewHandle,
 };
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
@@ -462,8 +462,10 @@ pub struct ChildView {
 }
 
 impl ChildView {
-    pub fn new(view_id: usize) -> Self {
-        Self { view_id }
+    pub fn new(view: impl Into<AnyViewHandle>) -> Self {
+        Self {
+            view_id: view.into().id(),
+        }
     }
 }
 

crates/outline/src/outline.rs 🔗

@@ -80,7 +80,7 @@ impl View for OutlineView {
 
         Flex::new(Axis::Vertical)
             .with_child(
-                Container::new(ChildView::new(self.query_editor.id()).boxed())
+                Container::new(ChildView::new(&self.query_editor).boxed())
                     .with_style(settings.theme.selector.input_editor.container)
                     .boxed(),
             )

crates/theme/src/theme.rs 🔗

@@ -24,6 +24,7 @@ pub struct Theme {
     pub project_panel: ProjectPanel,
     pub selector: Selector,
     pub editor: EditorStyle,
+    pub find: Find,
     pub project_diagnostics: ProjectDiagnostics,
 }
 
@@ -37,6 +38,7 @@ pub struct Workspace {
     pub left_sidebar: Sidebar,
     pub right_sidebar: Sidebar,
     pub status_bar: StatusBar,
+    pub toolbar: Toolbar,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -87,6 +89,33 @@ pub struct Tab {
     pub icon_conflict: Color,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct Toolbar {
+    pub height: f32,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct Find {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub editor: FindEditor,
+    pub invalid_editor: ContainerStyle,
+    pub mode_button_group: ContainerStyle,
+    pub mode_button: ContainedText,
+    pub active_mode_button: ContainedText,
+    pub hovered_mode_button: ContainedText,
+    pub active_hovered_mode_button: ContainedText,
+    pub match_background: Color,
+    pub match_index: ContainedText,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct FindEditor {
+    #[serde(flatten)]
+    pub input: InputEditorStyle,
+    pub max_width: f32,
+}
+
 #[derive(Deserialize, Default)]
 pub struct Sidebar {
     #[serde(flatten)]

crates/theme_selector/src/theme_selector.rs 🔗

@@ -299,7 +299,7 @@ impl View for ThemeSelector {
             ConstrainedBox::new(
                 Container::new(
                     Flex::new(Axis::Vertical)
-                        .with_child(ChildView::new(self.query_editor.id()).boxed())
+                        .with_child(ChildView::new(&self.query_editor).boxed())
                         .with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
                         .boxed(),
                 )

crates/workspace/src/pane.rs 🔗

@@ -7,11 +7,17 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::CursorStyle,
-    Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle,
+    AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
+    ViewHandle,
 };
 use postage::watch;
 use project::ProjectPath;
-use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
+use std::{
+    any::{Any, TypeId},
+    cell::RefCell,
+    cmp, mem,
+    rc::Rc,
+};
 use util::ResultExt;
 
 action!(Split, SplitDirection);
@@ -75,12 +81,29 @@ pub struct Pane {
     active_item_index: usize,
     settings: watch::Receiver<Settings>,
     nav_history: Rc<RefCell<NavHistory>>,
+    toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
+    active_toolbar_type: Option<TypeId>,
+    active_toolbar_visible: bool,
+}
+
+pub trait Toolbar: View {
+    fn active_item_changed(
+        &mut self,
+        item: Option<Box<dyn ItemViewHandle>>,
+        cx: &mut ViewContext<Self>,
+    ) -> bool;
+    fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
 }
 
-// #[derive(Debug, Eq, PartialEq)]
-// pub struct State {
-//     pub tabs: Vec<TabState>,
-// }
+trait ToolbarHandle {
+    fn active_item_changed(
+        &self,
+        item: Option<Box<dyn ItemViewHandle>>,
+        cx: &mut MutableAppContext,
+    ) -> bool;
+    fn on_dismiss(&self, cx: &mut MutableAppContext);
+    fn to_any(&self) -> AnyViewHandle;
+}
 
 pub struct ItemNavHistory {
     history: Rc<RefCell<NavHistory>>,
@@ -120,6 +143,9 @@ impl Pane {
             active_item_index: 0,
             settings,
             nav_history: Default::default(),
+            toolbars: Default::default(),
+            active_toolbar_type: Default::default(),
+            active_toolbar_visible: false,
         }
     }
 
@@ -288,9 +314,12 @@ impl Pane {
     pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
         if index < self.item_views.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
-            if prev_active_item_ix != self.active_item_index {
+            if prev_active_item_ix != self.active_item_index
+                && prev_active_item_ix < self.item_views.len()
+            {
                 self.item_views[prev_active_item_ix].1.deactivated(cx);
             }
+            self.update_active_toolbar(cx);
             self.focus_active_item(cx);
             cx.notify();
         }
@@ -344,14 +373,19 @@ impl Pane {
                 true
             }
         });
-        self.active_item_index = cmp::min(
-            self.active_item_index,
-            self.item_views.len().saturating_sub(1),
+        self.activate_item(
+            cmp::min(
+                self.active_item_index,
+                self.item_views.len().saturating_sub(1),
+            ),
+            cx,
         );
 
         if self.item_views.is_empty() {
+            self.update_active_toolbar(cx);
             cx.emit(Event::Remove);
         }
+
         cx.notify();
     }
 
@@ -365,6 +399,68 @@ impl Pane {
         cx.emit(Event::Split(direction));
     }
 
+    pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
+    where
+        F: FnOnce(&mut ViewContext<V>) -> V,
+        V: Toolbar,
+    {
+        let type_id = TypeId::of::<V>();
+        if self.active_toolbar_type != Some(type_id) {
+            self.dismiss_toolbar(cx);
+
+            let active_item = self.active_item();
+            self.toolbars
+                .entry(type_id)
+                .or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
+
+            self.active_toolbar_type = Some(type_id);
+            self.active_toolbar_visible =
+                self.toolbars[&type_id].active_item_changed(active_item, cx);
+            cx.notify();
+        }
+    }
+
+    pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
+            self.toolbars
+                .get_mut(&active_toolbar_type)
+                .unwrap()
+                .on_dismiss(cx);
+            self.active_toolbar_visible = false;
+            self.focus_active_item(cx);
+            cx.notify();
+        }
+    }
+
+    pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
+        self.toolbars
+            .get(&TypeId::of::<T>())
+            .and_then(|toolbar| toolbar.to_any().downcast())
+    }
+
+    pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
+        let type_id = self.active_toolbar_type?;
+        let toolbar = self.toolbars.get(&type_id)?;
+        if self.active_toolbar_visible {
+            Some(toolbar.to_any())
+        } else {
+            None
+        }
+    }
+
+    fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(type_id) = self.active_toolbar_type {
+            if let Some(toolbar) = self.toolbars.get(&type_id) {
+                self.active_toolbar_visible = toolbar.active_item_changed(
+                    self.item_views
+                        .get(self.active_item_index)
+                        .map(|i| i.1.clone()),
+                    cx,
+                );
+            }
+        }
+    }
+
     fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
         let settings = self.settings.borrow();
         let theme = &settings.theme;
@@ -516,7 +612,12 @@ impl View for Pane {
         if let Some(active_item) = self.active_item() {
             Flex::column()
                 .with_child(self.render_tabs(cx))
-                .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
+                .with_children(
+                    self.active_toolbar()
+                        .as_ref()
+                        .map(|view| ChildView::new(view).boxed()),
+                )
+                .with_child(ChildView::new(active_item).flexible(1., true).boxed())
                 .named("pane")
         } else {
             Empty::new().named("pane")
@@ -528,6 +629,24 @@ impl View for Pane {
     }
 }
 
+impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
+    fn active_item_changed(
+        &self,
+        item: Option<Box<dyn ItemViewHandle>>,
+        cx: &mut MutableAppContext,
+    ) -> bool {
+        self.update(cx, |this, cx| this.active_item_changed(item, cx))
+    }
+
+    fn on_dismiss(&self, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.on_dismiss(cx));
+    }
+
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
+    }
+}
+
 impl ItemNavHistory {
     pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
         Self {

crates/workspace/src/pane_group.rs 🔗

@@ -1,43 +1,45 @@
 use anyhow::{anyhow, Result};
-use gpui::{elements::*, Axis};
+use gpui::{elements::*, Axis, ViewHandle};
 use theme::Theme;
 
+use crate::Pane;
+
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct PaneGroup {
     root: Member,
 }
 
 impl PaneGroup {
-    pub fn new(pane_id: usize) -> Self {
+    pub fn new(pane: ViewHandle<Pane>) -> Self {
         Self {
-            root: Member::Pane(pane_id),
+            root: Member::Pane(pane),
         }
     }
 
     pub fn split(
         &mut self,
-        old_pane_id: usize,
-        new_pane_id: usize,
+        old_pane: &ViewHandle<Pane>,
+        new_pane: &ViewHandle<Pane>,
         direction: SplitDirection,
     ) -> Result<()> {
         match &mut self.root {
-            Member::Pane(pane_id) => {
-                if *pane_id == old_pane_id {
-                    self.root = Member::new_axis(old_pane_id, new_pane_id, direction);
+            Member::Pane(pane) => {
+                if pane == old_pane {
+                    self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
                     Ok(())
                 } else {
                     Err(anyhow!("Pane not found"))
                 }
             }
-            Member::Axis(axis) => axis.split(old_pane_id, new_pane_id, direction),
+            Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
         }
     }
 
-    pub fn remove(&mut self, pane_id: usize) -> Result<bool> {
+    pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
         match &mut self.root {
             Member::Pane(_) => Ok(false),
             Member::Axis(axis) => {
-                if let Some(last_pane) = axis.remove(pane_id)? {
+                if let Some(last_pane) = axis.remove(pane)? {
                     self.root = last_pane;
                 }
                 Ok(true)
@@ -53,11 +55,15 @@ impl PaneGroup {
 #[derive(Clone, Debug, Eq, PartialEq)]
 enum Member {
     Axis(PaneAxis),
-    Pane(usize),
+    Pane(ViewHandle<Pane>),
 }
 
 impl Member {
-    fn new_axis(old_pane_id: usize, new_pane_id: usize, direction: SplitDirection) -> Self {
+    fn new_axis(
+        old_pane: ViewHandle<Pane>,
+        new_pane: ViewHandle<Pane>,
+        direction: SplitDirection,
+    ) -> Self {
         use Axis::*;
         use SplitDirection::*;
 
@@ -67,16 +73,16 @@ impl Member {
         };
 
         let members = match direction {
-            Up | Left => vec![Member::Pane(new_pane_id), Member::Pane(old_pane_id)],
-            Down | Right => vec![Member::Pane(old_pane_id), Member::Pane(new_pane_id)],
+            Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)],
+            Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)],
         };
 
         Member::Axis(PaneAxis { axis, members })
     }
 
-    pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
+    pub fn render(&self, theme: &Theme) -> ElementBox {
         match self {
-            Member::Pane(view_id) => ChildView::new(*view_id).boxed(),
+            Member::Pane(pane) => ChildView::new(pane).boxed(),
             Member::Axis(axis) => axis.render(theme),
         }
     }
@@ -91,8 +97,8 @@ struct PaneAxis {
 impl PaneAxis {
     fn split(
         &mut self,
-        old_pane_id: usize,
-        new_pane_id: usize,
+        old_pane: &ViewHandle<Pane>,
+        new_pane: &ViewHandle<Pane>,
         direction: SplitDirection,
     ) -> Result<()> {
         use SplitDirection::*;
@@ -100,23 +106,24 @@ impl PaneAxis {
         for (idx, member) in self.members.iter_mut().enumerate() {
             match member {
                 Member::Axis(axis) => {
-                    if axis.split(old_pane_id, new_pane_id, direction).is_ok() {
+                    if axis.split(old_pane, new_pane, direction).is_ok() {
                         return Ok(());
                     }
                 }
-                Member::Pane(pane_id) => {
-                    if *pane_id == old_pane_id {
+                Member::Pane(pane) => {
+                    if pane == old_pane {
                         if direction.matches_axis(self.axis) {
                             match direction {
                                 Up | Left => {
-                                    self.members.insert(idx, Member::Pane(new_pane_id));
+                                    self.members.insert(idx, Member::Pane(new_pane.clone()));
                                 }
                                 Down | Right => {
-                                    self.members.insert(idx + 1, Member::Pane(new_pane_id));
+                                    self.members.insert(idx + 1, Member::Pane(new_pane.clone()));
                                 }
                             }
                         } else {
-                            *member = Member::new_axis(old_pane_id, new_pane_id, direction);
+                            *member =
+                                Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
                         }
                         return Ok(());
                     }
@@ -126,13 +133,13 @@ impl PaneAxis {
         Err(anyhow!("Pane not found"))
     }
 
-    fn remove(&mut self, pane_id_to_remove: usize) -> Result<Option<Member>> {
+    fn remove(&mut self, pane_to_remove: &ViewHandle<Pane>) -> Result<Option<Member>> {
         let mut found_pane = false;
         let mut remove_member = None;
         for (idx, member) in self.members.iter_mut().enumerate() {
             match member {
                 Member::Axis(axis) => {
-                    if let Ok(last_pane) = axis.remove(pane_id_to_remove) {
+                    if let Ok(last_pane) = axis.remove(pane_to_remove) {
                         if let Some(last_pane) = last_pane {
                             *member = last_pane;
                         }
@@ -140,8 +147,8 @@ impl PaneAxis {
                         break;
                     }
                 }
-                Member::Pane(pane_id) => {
-                    if *pane_id == pane_id_to_remove {
+                Member::Pane(pane) => {
+                    if pane == pane_to_remove {
                         found_pane = true;
                         remove_member = Some(idx);
                         break;

crates/workspace/src/sidebar.rs 🔗

@@ -136,7 +136,7 @@ impl Sidebar {
 
             container.add_child(
                 Hook::new(
-                    ConstrainedBox::new(ChildView::new(active_item.id()).boxed())
+                    ConstrainedBox::new(ChildView::new(active_item).boxed())
                         .with_max_width(*self.width.borrow())
                         .boxed(),
                 )

crates/workspace/src/status_bar.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{ItemViewHandle, Pane, Settings};
 use gpui::{
-    elements::*, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View,
-    ViewContext, ViewHandle,
+    elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
+    View, ViewContext, ViewHandle,
 };
 use postage::watch;
 
@@ -14,7 +14,7 @@ pub trait StatusItemView: View {
 }
 
 trait StatusItemViewHandle {
-    fn id(&self) -> usize;
+    fn to_any(&self) -> AnyViewHandle;
     fn set_active_pane_item(
         &self,
         active_pane_item: Option<&dyn ItemViewHandle>,
@@ -45,13 +45,13 @@ impl View for StatusBar {
             .with_children(
                 self.left_items
                     .iter()
-                    .map(|i| ChildView::new(i.id()).aligned().boxed()),
+                    .map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
             )
             .with_child(Empty::new().flexible(1., true).boxed())
             .with_children(
                 self.right_items
                     .iter()
-                    .map(|i| ChildView::new(i.id()).aligned().boxed()),
+                    .map(|i| ChildView::new(i.as_ref()).aligned().boxed()),
             )
             .contained()
             .with_style(theme.container)
@@ -111,8 +111,8 @@ impl StatusBar {
 }
 
 impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
-    fn id(&self) -> usize {
-        self.id()
+    fn to_any(&self) -> AnyViewHandle {
+        self.into()
     }
 
     fn set_active_pane_item(
@@ -125,3 +125,9 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
         });
     }
 }
+
+impl Into<AnyViewHandle> for &dyn StatusItemViewHandle {
+    fn into(self) -> AnyViewHandle {
+        self.to_any()
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -599,7 +599,7 @@ impl Workspace {
         Workspace {
             modal: None,
             weak_self: cx.weak_handle(),
-            center: PaneGroup::new(pane.id()),
+            center: PaneGroup::new(pane.clone()),
             panes: vec![pane.clone()],
             active_pane: pane.clone(),
             status_bar,
@@ -1048,15 +1048,13 @@ impl Workspace {
                 new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx));
             }
         }
-        self.center
-            .split(pane.id(), new_pane.id(), direction)
-            .unwrap();
+        self.center.split(&pane, &new_pane, direction).unwrap();
         cx.notify();
         new_pane
     }
 
     fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
-        if self.center.remove(pane.id()).unwrap() {
+        if self.center.remove(&pane).unwrap() {
             self.panes.retain(|p| p != &pane);
             self.activate_pane(self.panes.last().unwrap().clone(), cx);
         }
@@ -1287,7 +1285,7 @@ impl View for Workspace {
                                     Flexible::new(1., true, self.center.render(&settings.theme))
                                         .boxed(),
                                 )
-                                .with_child(ChildView::new(self.status_bar.id()).boxed())
+                                .with_child(ChildView::new(&self.status_bar).boxed())
                                 .flexible(1., true)
                                 .boxed(),
                         );
@@ -1298,7 +1296,7 @@ impl View for Workspace {
                         content.add_child(self.right_sidebar.render(&settings, cx));
                         content.boxed()
                     })
-                    .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
+                    .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
                     .flexible(1.0, true)
                     .boxed(),
             )

crates/zed/Cargo.toml 🔗

@@ -36,6 +36,7 @@ contacts_panel = { path = "../contacts_panel" }
 diagnostics = { path = "../diagnostics" }
 editor = { path = "../editor" }
 file_finder = { path = "../file_finder" }
+find = { path = "../find" }
 fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 go_to_line = { path = "../go_to_line" }

crates/zed/assets/themes/_base.toml 🔗

@@ -81,6 +81,9 @@ item_spacing = 24
 cursor_position = "$text.2"
 diagnostic_message = "$text.2"
 
+[workspace.toolbar]
+height = 44
+
 [panel]
 padding = { top = 12, left = 12, bottom = 12, right = 12 }
 
@@ -318,3 +321,50 @@ status_bar_item = { extends = "$text.2", margin.right = 10 }
 tab_icon_width = 13
 tab_icon_spacing = 4
 tab_summary_spacing = 10
+
+[find]
+match_background = "$state.highlighted_line"
+background = "$surface.1"
+
+[find.mode_button]
+extends = "$text.1"
+padding = { left = 6, right = 6, top = 1, bottom = 1 }
+corner_radius = 6
+background = "$surface.1"
+border = { width = 1, color = "$border.0" }
+margin.left = 1
+margin.right = 1
+
+[find.mode_button_group]
+padding = { left = 2, right = 2 }
+
+[find.active_mode_button]
+extends = "$find.mode_button"
+background = "$surface.2"
+
+[find.hovered_mode_button]
+extends = "$find.mode_button"
+background = "$surface.2"
+
+[find.active_hovered_mode_button]
+extends = "$find.mode_button"
+background = "$surface.2"
+
+[find.match_index]
+extends = "$text.1"
+padding = 6
+
+[find.editor]
+max_width = 400
+background = "$surface.0"
+corner_radius = 6
+padding = { left = 13, right = 13, top = 3, bottom = 3 }
+margin = { top = 5, bottom = 5, left = 5, right = 5 }
+text = "$text.0"
+placeholder_text = "$text.2"
+selection = "$selection.host"
+border = { width = 1, color = "$border.0" }
+
+[find.invalid_editor]
+extends = "$find.editor"
+border = { width = 1, color = "$status.bad" }

crates/zed/src/main.rs 🔗

@@ -62,6 +62,7 @@ fn main() {
         outline::init(cx);
         project_panel::init(cx);
         diagnostics::init(cx);
+        find::init(cx);
         cx.spawn({
             let client = client.clone();
             |cx| async move {