Merge pull request #1587 from zed-industries/alac-search

Mikayla Maki created

Terminal Search

Change summary

crates/diagnostics/src/items.rs                |   2 
crates/editor/src/items.rs                     | 101 ++---
crates/language/src/syntax_map.rs              |   1 
crates/search/src/buffer_search.rs             | 110 +++--
crates/search/src/project_search.rs            |  22 
crates/terminal/src/modal.rs                   |   7 
crates/terminal/src/terminal.rs                | 386 ++++++++++++++-----
crates/terminal/src/terminal_container_view.rs | 140 ++++++
crates/terminal/src/terminal_element.rs        | 279 ++++++-------
crates/terminal/src/terminal_view.rs           |  33 +
crates/workspace/src/searchable.rs             | 112 +++-
styles/package-lock.json                       |   1 
12 files changed, 774 insertions(+), 420 deletions(-)

Detailed changes

crates/diagnostics/src/items.rs πŸ”—

@@ -179,7 +179,7 @@ impl View for DiagnosticIndicator {
         if in_progress {
             element.add_child(
                 Label::new(
-                    "checking…".into(),
+                    "Checking…".into(),
                     style.diagnostic_message.default.text.clone(),
                 )
                 .aligned()

crates/editor/src/items.rs πŸ”—

@@ -513,17 +513,17 @@ impl SearchableItem for Editor {
 
     fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
         match event {
-            Event::BufferEdited => Some(SearchEvent::ContentsUpdated),
-            Event::SelectionsChanged { .. } => Some(SearchEvent::SelectionsChanged),
+            Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
+            Event::SelectionsChanged { .. } => Some(SearchEvent::ActiveMatchChanged),
             _ => None,
         }
     }
 
-    fn clear_highlights(&mut self, cx: &mut ViewContext<Self>) {
+    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
         self.clear_background_highlights::<BufferSearchHighlights>(cx);
     }
 
-    fn highlight_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
+    fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
         self.highlight_background::<BufferSearchHighlights>(
             matches,
             |theme| theme.search.match_background,
@@ -553,40 +553,56 @@ impl SearchableItem for Editor {
         }
     }
 
-    fn select_next_match_in_direction(
+    fn activate_match(
         &mut self,
         index: usize,
-        direction: Direction,
         matches: Vec<Range<Anchor>>,
         cx: &mut ViewContext<Self>,
     ) {
-        let new_index: usize = match_index_for_direction(
-            matches.as_slice(),
-            &self.selections.newest_anchor().head(),
-            index,
-            direction,
-            &self.buffer().read(cx).snapshot(cx),
-        );
-
-        let range_to_select = matches[new_index].clone();
-        self.unfold_ranges([range_to_select.clone()], false, cx);
+        self.unfold_ranges([matches[index].clone()], false, cx);
         self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-            s.select_ranges([range_to_select])
+            s.select_ranges([matches[index].clone()])
         });
     }
 
-    fn select_match_by_index(
+    fn match_index_for_direction(
         &mut self,
-        index: usize,
-        matches: Vec<Range<Anchor>>,
+        matches: &Vec<Range<Anchor>>,
+        mut current_index: usize,
+        direction: Direction,
         cx: &mut ViewContext<Self>,
-    ) {
-        self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-            s.select_ranges([matches[index].clone()])
-        });
+    ) -> usize {
+        let buffer = self.buffer().read(cx).snapshot(cx);
+        let cursor = self.selections.newest_anchor().head();
+        if matches[current_index].start.cmp(&cursor, &buffer).is_gt() {
+            if direction == Direction::Prev {
+                if current_index == 0 {
+                    current_index = matches.len() - 1;
+                } else {
+                    current_index -= 1;
+                }
+            }
+        } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() {
+            if direction == Direction::Next {
+                current_index = 0;
+            }
+        } else if direction == Direction::Prev {
+            if current_index == 0 {
+                current_index = matches.len() - 1;
+            } else {
+                current_index -= 1;
+            }
+        } else if direction == Direction::Next {
+            if current_index == matches.len() - 1 {
+                current_index = 0
+            } else {
+                current_index += 1;
+            }
+        };
+        current_index
     }
 
-    fn matches(
+    fn find_matches(
         &mut self,
         query: project::search::SearchQuery,
         cx: &mut ViewContext<Self>,
@@ -637,41 +653,6 @@ impl SearchableItem for Editor {
     }
 }
 
-pub fn match_index_for_direction(
-    ranges: &[Range<Anchor>],
-    cursor: &Anchor,
-    mut index: usize,
-    direction: Direction,
-    buffer: &MultiBufferSnapshot,
-) -> usize {
-    if ranges[index].start.cmp(cursor, buffer).is_gt() {
-        if direction == Direction::Prev {
-            if index == 0 {
-                index = ranges.len() - 1;
-            } else {
-                index -= 1;
-            }
-        }
-    } else if ranges[index].end.cmp(cursor, buffer).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;
-        }
-    };
-    index
-}
-
 pub fn active_match_index(
     ranges: &[Range<Anchor>],
     cursor: &Anchor,

crates/language/src/syntax_map.rs πŸ”—

@@ -233,7 +233,6 @@ impl SyntaxSnapshot {
             };
             let (start_byte, start_point) = layer.range.start.summary::<(usize, Point)>(text);
 
-
             // Ignore edits that end before the start of this layer, and don't consider them
             // for any subsequent layers at this same depth.
             loop {

crates/search/src/buffer_search.rs πŸ”—

@@ -95,6 +95,12 @@ impl View for BufferSearchBar {
         } else {
             theme.search.editor.input.container
         };
+        let supported_options = self
+            .active_searchable_item
+            .as_ref()
+            .map(|active_searchable_item| active_searchable_item.supported_options())
+            .unwrap_or_default();
+
         Flex::row()
             .with_child(
                 Flex::row()
@@ -143,9 +149,24 @@ impl View for BufferSearchBar {
             )
             .with_child(
                 Flex::row()
-                    .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx))
-                    .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx))
-                    .with_child(self.render_search_option("Regex", SearchOption::Regex, cx))
+                    .with_children(self.render_search_option(
+                        supported_options.case,
+                        "Case",
+                        SearchOption::CaseSensitive,
+                        cx,
+                    ))
+                    .with_children(self.render_search_option(
+                        supported_options.word,
+                        "Word",
+                        SearchOption::WholeWord,
+                        cx,
+                    ))
+                    .with_children(self.render_search_option(
+                        supported_options.regex,
+                        "Regex",
+                        SearchOption::Regex,
+                        cx,
+                    ))
                     .contained()
                     .with_style(theme.search.option_button_group)
                     .aligned()
@@ -174,7 +195,9 @@ impl ToolbarItemView for BufferSearchBar {
                 cx,
                 Box::new(move |search_event, cx| {
                     if let Some(this) = handle.upgrade(cx) {
-                        this.update(cx, |this, cx| this.on_active_editor_event(search_event, cx));
+                        this.update(cx, |this, cx| {
+                            this.on_active_searchable_item_event(search_event, cx)
+                        });
                     }
                 }),
             ));
@@ -232,7 +255,7 @@ impl BufferSearchBar {
             if let Some(searchable_item) =
                 WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
             {
-                searchable_item.clear_highlights(cx);
+                searchable_item.clear_matches(cx);
             }
         }
         if let Some(active_editor) = self.active_searchable_item.as_ref() {
@@ -281,36 +304,43 @@ impl BufferSearchBar {
 
     fn render_search_option(
         &self,
+        option_supported: bool,
         icon: &str,
         option: SearchOption,
         cx: &mut RenderContext<Self>,
-    ) -> ElementBox {
+    ) -> Option<ElementBox> {
+        if !option_supported {
+            return None;
+        }
+
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let is_active = self.is_search_option_enabled(option);
-        MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
-            let style = &cx
-                .global::<Settings>()
-                .theme
-                .search
-                .option_button
-                .style_for(state, is_active);
-            Label::new(icon.to_string(), style.text.clone())
-                .contained()
-                .with_style(style.container)
-                .boxed()
-        })
-        .on_click(MouseButton::Left, move |_, cx| {
-            cx.dispatch_any_action(option.to_toggle_action())
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .with_tooltip::<Self, _>(
-            option as usize,
-            format!("Toggle {}", option.label()),
-            Some(option.to_toggle_action()),
-            tooltip_style,
-            cx,
+        Some(
+            MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+                let style = &cx
+                    .global::<Settings>()
+                    .theme
+                    .search
+                    .option_button
+                    .style_for(state, is_active);
+                Label::new(icon.to_string(), style.text.clone())
+                    .contained()
+                    .with_style(style.container)
+                    .boxed()
+            })
+            .on_click(MouseButton::Left, move |_, cx| {
+                cx.dispatch_any_action(option.to_toggle_action())
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .with_tooltip::<Self, _>(
+                option as usize,
+                format!("Toggle {}", option.label()),
+                Some(option.to_toggle_action()),
+                tooltip_style,
+                cx,
+            )
+            .boxed(),
         )
-        .boxed()
     }
 
     fn render_nav_button(
@@ -420,8 +450,10 @@ impl BufferSearchBar {
                     .seachable_items_with_matches
                     .get(&searchable_item.downgrade())
                 {
-                    searchable_item.select_next_match_in_direction(index, direction, matches, cx);
-                    searchable_item.highlight_matches(matches, cx);
+                    let new_match_index =
+                        searchable_item.match_index_for_direction(matches, index, direction, cx);
+                    searchable_item.update_matches(matches, cx);
+                    searchable_item.activate_match(new_match_index, matches, cx);
                 }
             }
         }
@@ -461,10 +493,10 @@ impl BufferSearchBar {
         }
     }
 
-    fn on_active_editor_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
+    fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
         match event {
-            SearchEvent::ContentsUpdated => self.update_matches(false, cx),
-            SearchEvent::SelectionsChanged => self.update_match_index(cx),
+            SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
+            SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
         }
     }
 
@@ -477,7 +509,7 @@ impl BufferSearchBar {
                 if Some(&searchable_item) == self.active_searchable_item.as_ref() {
                     active_item_matches = Some((searchable_item.downgrade(), matches));
                 } else {
-                    searchable_item.clear_highlights(cx);
+                    searchable_item.clear_matches(cx);
                 }
             }
         }
@@ -492,7 +524,7 @@ impl BufferSearchBar {
         if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
             if query.is_empty() {
                 self.active_match_index.take();
-                active_searchable_item.clear_highlights(cx);
+                active_searchable_item.clear_matches(cx);
             } else {
                 let query = if self.regex {
                     match SearchQuery::regex(query, self.whole_word, self.case_sensitive) {
@@ -507,7 +539,7 @@ impl BufferSearchBar {
                     SearchQuery::text(query, self.whole_word, self.case_sensitive)
                 };
 
-                let matches = active_searchable_item.matches(query, cx);
+                let matches = active_searchable_item.find_matches(query, cx);
 
                 let active_searchable_item = active_searchable_item.downgrade();
                 self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
@@ -527,13 +559,13 @@ impl BufferSearchBar {
                                         .seachable_items_with_matches
                                         .get(&active_searchable_item.downgrade())
                                         .unwrap();
+                                    active_searchable_item.update_matches(matches, cx);
                                     if select_closest_match {
                                         if let Some(match_ix) = this.active_match_index {
                                             active_searchable_item
-                                                .select_match_by_index(match_ix, matches, cx);
+                                                .activate_match(match_ix, matches, cx);
                                         }
                                     }
-                                    active_searchable_item.highlight_matches(matches, cx);
                                 }
                                 cx.notify();
                             }

crates/search/src/project_search.rs πŸ”—

@@ -4,8 +4,8 @@ use crate::{
 };
 use collections::HashMap;
 use editor::{
-    items::{active_match_index, match_index_for_direction},
-    Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
+    items::active_match_index, Anchor, Autoscroll, Editor, MultiBuffer, SelectAll,
+    MAX_TAB_TITLE_LEN,
 };
 use gpui::{
     actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox,
@@ -23,7 +23,7 @@ use std::{
 };
 use util::ResultExt as _;
 use workspace::{
-    searchable::{Direction, SearchableItemHandle},
+    searchable::{Direction, SearchableItem, SearchableItemHandle},
     Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
 };
 
@@ -486,16 +486,12 @@ impl ProjectSearchView {
 
     fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
         if let Some(index) = self.active_match_index {
-            let model = self.model.read(cx);
-            let results_editor = self.results_editor.read(cx);
-            let new_index = match_index_for_direction(
-                &model.match_ranges,
-                &results_editor.selections.newest_anchor().head(),
-                index,
-                direction,
-                &results_editor.buffer().read(cx).snapshot(cx),
-            );
-            let range_to_select = model.match_ranges[new_index].clone();
+            let match_ranges = self.model.read(cx).match_ranges.clone();
+            let new_index = self.results_editor.update(cx, |editor, cx| {
+                editor.match_index_for_direction(&match_ranges, index, direction, cx)
+            });
+
+            let range_to_select = match_ranges[new_index].clone();
             self.results_editor.update(cx, |editor, cx| {
                 editor.unfold_ranges([range_to_select.clone()], false, cx);
                 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {

crates/terminal/src/modal.rs πŸ”—

@@ -4,7 +4,7 @@ use workspace::Workspace;
 
 use crate::{
     terminal_container_view::{
-        get_working_directory, DeployModal, TerminalContainer, TerminalContent,
+        get_working_directory, DeployModal, TerminalContainer, TerminalContainerContent,
     },
     Event, Terminal,
 };
@@ -42,7 +42,7 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
 
             let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
 
-            if let TerminalContent::Connected(connected) = &this.read(cx).content {
+            if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
                 let terminal_handle = connected.read(cx).handle();
                 cx.subscribe(&terminal_handle, on_event).detach();
                 // Set the global immediately if terminal construction was successful,
@@ -55,7 +55,8 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon
             this
         }) {
             // Terminal modal was dismissed. Store terminal if the terminal view is connected
-            if let TerminalContent::Connected(connected) = &closed_terminal_handle.read(cx).content
+            if let TerminalContainerContent::Connected(connected) =
+                &closed_terminal_handle.read(cx).content
             {
                 let terminal_handle = connected.read(cx).handle();
                 // Set the global immediately if terminal construction was successful,

crates/terminal/src/terminal.rs πŸ”—

@@ -9,15 +9,21 @@ use alacritty_terminal::{
     config::{Config, Program, PtyConfig, Scrolling},
     event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
     event_loop::{EventLoop, Msg, Notifier},
-    grid::{Dimensions, Scroll},
-    index::{Direction, Point},
-    selection::{Selection, SelectionType},
+    grid::{Dimensions, Scroll as AlacScroll},
+    index::{Column, Direction as AlacDirection, Line, Point},
+    selection::{Selection, SelectionRange, SelectionType},
     sync::FairMutex,
-    term::{RenderableContent, TermMode},
+    term::{
+        cell::Cell,
+        color::Rgb,
+        search::{Match, RegexIter, RegexSearch},
+        RenderableCursor, TermMode,
+    },
     tty::{self, setup_env},
     Term,
 };
 use anyhow::{bail, Result};
+
 use futures::{
     channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
     FutureExt,
@@ -27,14 +33,15 @@ use mappings::mouse::{
     alt_scroll, mouse_button_report, mouse_moved_report, mouse_point, mouse_side, scroll_report,
 };
 use modal::deploy_modal;
+
 use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
 use std::{
     collections::{HashMap, VecDeque},
     fmt::Display,
-    ops::Sub,
+    ops::{Deref, RangeInclusive, Sub},
     path::PathBuf,
     sync::Arc,
-    time::Duration,
+    time::{Duration, Instant},
 };
 use thiserror::Error;
 
@@ -43,7 +50,7 @@ use gpui::{
     keymap::Keystroke,
     scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
     ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
-    ScrollWheelEvent,
+    ScrollWheelEvent, Task,
 };
 
 use crate::mappings::{
@@ -62,8 +69,8 @@ pub fn init(cx: &mut MutableAppContext) {
 ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
 ///Scroll multiplier that is set to 3 by default. This will be removed when I
 ///Implement scroll bars.
-pub const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
-
+const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.;
+const MAX_SEARCH_LINES: usize = 100;
 const DEBUG_TERMINAL_WIDTH: f32 = 500.;
 const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
 const DEBUG_CELL_WIDTH: f32 = 5.;
@@ -77,15 +84,18 @@ pub enum Event {
     Bell,
     Wakeup,
     BlinkChanged,
+    SelectionsChanged,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone)]
 enum InternalEvent {
-    TermEvent(AlacTermEvent),
+    ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
     Resize(TerminalSize),
     Clear,
-    Scroll(Scroll),
-    SetSelection(Option<Selection>),
+    // FocusNextMatch,
+    Scroll(AlacScroll),
+    ScrollToPoint(Point),
+    SetSelection(Option<(Selection, Point)>),
     UpdateSelection(Vector2F),
     Copy,
 }
@@ -164,8 +174,12 @@ impl From<TerminalSize> for WindowSize {
 }
 
 impl Dimensions for TerminalSize {
+    /// Note: this is supposed to be for the back buffer's length,
+    /// but we exclusively use it to resize the terminal, which does not
+    /// use this method. We still have to implement it for the trait though,
+    /// hence, this comment.
     fn total_lines(&self) -> usize {
-        self.screen_lines() //TODO: Check that this is fine. This is supposed to be for the back buffer...
+        self.screen_lines()
     }
 
     fn screen_lines(&self) -> usize {
@@ -366,11 +380,13 @@ impl TerminalBuilder {
             events: VecDeque::with_capacity(10), //Should never get this high.
             title: shell_txt.clone(),
             default_title: shell_txt,
-            last_mode: TermMode::NONE,
+            last_content: Default::default(),
             cur_size: initial_size,
             last_mouse: None,
-            last_offset: 0,
-            current_selection: false,
+            matches: Vec::new(),
+            last_synced: Instant::now(),
+            sync_task: None,
+            selection_head: None,
         };
 
         Ok(TerminalBuilder {
@@ -432,17 +448,62 @@ impl TerminalBuilder {
     }
 }
 
+#[derive(Debug, Clone)]
+struct IndexedCell {
+    point: Point,
+    cell: Cell,
+}
+
+impl Deref for IndexedCell {
+    type Target = Cell;
+
+    #[inline]
+    fn deref(&self) -> &Cell {
+        &self.cell
+    }
+}
+
+#[derive(Clone)]
+pub struct TerminalContent {
+    cells: Vec<IndexedCell>,
+    mode: TermMode,
+    display_offset: usize,
+    selection_text: Option<String>,
+    selection: Option<SelectionRange>,
+    cursor: RenderableCursor,
+    cursor_char: char,
+}
+
+impl Default for TerminalContent {
+    fn default() -> Self {
+        TerminalContent {
+            cells: Default::default(),
+            mode: Default::default(),
+            display_offset: Default::default(),
+            selection_text: Default::default(),
+            selection: Default::default(),
+            cursor: RenderableCursor {
+                shape: alacritty_terminal::ansi::CursorShape::Block,
+                point: Point::new(Line(0), Column(0)),
+            },
+            cursor_char: Default::default(),
+        }
+    }
+}
+
 pub struct Terminal {
     pty_tx: Notifier,
     term: Arc<FairMutex<Term<ZedListener>>>,
     events: VecDeque<InternalEvent>,
     default_title: String,
     title: String,
+    last_mouse: Option<(Point, AlacDirection)>,
+    pub matches: Vec<RangeInclusive<Point>>,
     cur_size: TerminalSize,
-    last_mode: TermMode,
-    last_offset: usize,
-    last_mouse: Option<(Point, Direction)>,
-    current_selection: bool,
+    last_content: TerminalContent,
+    last_synced: Instant,
+    sync_task: Option<Task<()>>,
+    selection_head: Option<Point>,
 }
 
 impl Terminal {
@@ -482,9 +543,11 @@ impl Terminal {
                 cx.emit(Event::Wakeup);
                 cx.notify();
             }
-            AlacTermEvent::ColorRequest(_, _) => self
-                .events
-                .push_back(InternalEvent::TermEvent(event.clone())),
+            AlacTermEvent::ColorRequest(idx, fun_ptr) => {
+                self.events
+                    .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
+                cx.notify(); //Immediately schedule a render to respond to the color request
+            }
         }
     }
 
@@ -496,14 +559,12 @@ impl Terminal {
         cx: &mut ModelContext<Self>,
     ) {
         match event {
-            InternalEvent::TermEvent(term_event) => {
-                if let AlacTermEvent::ColorRequest(index, format) = term_event {
-                    let color = term.colors()[*index].unwrap_or_else(|| {
-                        let term_style = &cx.global::<Settings>().theme.terminal;
-                        to_alac_rgb(get_color_at_index(index, &term_style.colors))
-                    });
-                    self.write_to_pty(format(color))
-                }
+            InternalEvent::ColorRequest(index, format) => {
+                let color = term.colors()[*index].unwrap_or_else(|| {
+                    let term_style = &cx.global::<Settings>().theme.terminal;
+                    to_alac_rgb(get_color_at_index(index, &term_style.colors))
+                });
+                self.write_to_pty(format(color))
             }
             InternalEvent::Resize(new_size) => {
                 self.cur_size = *new_size;
@@ -519,7 +580,14 @@ impl Terminal {
             InternalEvent::Scroll(scroll) => {
                 term.scroll_display(*scroll);
             }
-            InternalEvent::SetSelection(sel) => term.selection = sel.clone(),
+            InternalEvent::SetSelection(selection) => {
+                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
+
+                if let Some((_, head)) = selection {
+                    self.selection_head = Some(*head);
+                }
+                cx.emit(Event::SelectionsChanged)
+            }
             InternalEvent::UpdateSelection(position) => {
                 if let Some(mut selection) = term.selection.take() {
                     let point = mouse_point(*position, self.cur_size, term.grid().display_offset());
@@ -527,6 +595,9 @@ impl Terminal {
 
                     selection.update(point, side);
                     term.selection = Some(selection);
+
+                    self.selection_head = Some(point);
+                    cx.emit(Event::SelectionsChanged)
                 }
             }
 
@@ -535,27 +606,30 @@ impl Terminal {
                     cx.write_to_clipboard(ClipboardItem::new(txt))
                 }
             }
+            InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point),
         }
     }
 
-    fn begin_select(&mut self, sel: Selection) {
-        self.current_selection = true;
-        self.events
-            .push_back(InternalEvent::SetSelection(Some(sel)));
+    pub fn last_content(&self) -> &TerminalContent {
+        &self.last_content
     }
 
-    fn continue_selection(&mut self, location: Vector2F) {
-        self.events
-            .push_back(InternalEvent::UpdateSelection(location))
-    }
+    //To test:
+    //- Activate match on terminal (scrolling and selection)
+    //- Editor search snapping behavior
 
-    fn end_select(&mut self) {
-        self.current_selection = false;
-        self.events.push_back(InternalEvent::SetSelection(None));
+    pub fn activate_match(&mut self, index: usize) {
+        if let Some(search_match) = self.matches.get(index).cloned() {
+            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
+
+            self.events
+                .push_back(InternalEvent::ScrollToPoint(*search_match.start()));
+        }
     }
 
-    fn scroll(&mut self, scroll: Scroll) {
-        self.events.push_back(InternalEvent::Scroll(scroll));
+    fn set_selection(&mut self, selection: Option<(Selection, Point)>) {
+        self.events
+            .push_back(InternalEvent::SetSelection(selection));
     }
 
     pub fn copy(&mut self) {
@@ -577,13 +651,15 @@ impl Terminal {
     }
 
     pub fn input(&mut self, input: String) {
-        self.scroll(Scroll::Bottom);
-        self.end_select();
+        self.events
+            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
+        self.events.push_back(InternalEvent::SetSelection(None));
+
         self.write_to_pty(input);
     }
 
     pub fn try_keystroke(&mut self, keystroke: &Keystroke) -> bool {
-        let esc = to_esc_str(keystroke, &self.last_mode);
+        let esc = to_esc_str(keystroke, &self.last_content.mode);
         if let Some(esc) = esc {
             self.input(esc);
             true
@@ -594,7 +670,7 @@ impl Terminal {
 
     ///Paste text into the terminal
     pub fn paste(&mut self, text: &str) {
-        let paste_text = if self.last_mode.contains(TermMode::BRACKETED_PASTE) {
+        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
             format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
         } else {
             text.replace("\r\n", "\r").replace('\n', "\r")
@@ -602,42 +678,81 @@ impl Terminal {
         self.input(paste_text)
     }
 
-    pub fn render_lock<F, T>(&mut self, cx: &mut ModelContext<Self>, f: F) -> T
-    where
-        F: FnOnce(RenderableContent, char) -> T,
-    {
-        let m = self.term.clone(); //Arc clone
-        let mut term = m.lock();
+    pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
+        let term = self.term.clone();
+
+        let mut terminal = if let Some(term) = term.try_lock_unfair() {
+            term
+        } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
+            term.lock_unfair()
+        } else if let None = self.sync_task {
+            //Skip this frame
+            let delay = cx.background().timer(Duration::from_millis(16));
+            self.sync_task = Some(cx.spawn_weak(|weak_handle, mut cx| async move {
+                delay.await;
+                cx.update(|cx| {
+                    if let Some(handle) = weak_handle.upgrade(cx) {
+                        handle.update(cx, |terminal, cx| {
+                            terminal.sync_task.take();
+                            cx.notify();
+                        });
+                    }
+                });
+            }));
+            return;
+        } else {
+            //No lock and delayed rendering already scheduled, nothing to do
+            return;
+        };
 
-        //Note that this ordering matters for
+        //Note that this ordering matters for event processing
         while let Some(e) = self.events.pop_front() {
-            self.process_terminal_event(&e, &mut term, cx)
+            self.process_terminal_event(&e, &mut terminal, cx)
         }
 
-        self.last_mode = *term.mode();
+        self.last_content = Self::make_content(&terminal);
+        self.last_synced = Instant::now();
+    }
 
+    fn make_content(term: &Term<ZedListener>) -> TerminalContent {
         let content = term.renderable_content();
-
-        self.last_offset = content.display_offset;
-
-        let cursor_text = term.grid()[content.cursor.point].c;
-
-        f(content, cursor_text)
+        TerminalContent {
+            cells: content
+                .display_iter
+                //TODO: Add this once there's a way to retain empty lines
+                // .filter(|ic| {
+                //     !ic.flags.contains(Flags::HIDDEN)
+                //         && !(ic.bg == Named(NamedColor::Background)
+                //             && ic.c == ' '
+                //             && !ic.flags.contains(Flags::INVERSE))
+                // })
+                .map(|ic| IndexedCell {
+                    point: ic.point,
+                    cell: ic.cell.clone(),
+                })
+                .collect::<Vec<IndexedCell>>(),
+            mode: content.mode,
+            display_offset: content.display_offset,
+            selection_text: term.selection_to_string(),
+            selection: content.selection,
+            cursor: content.cursor,
+            cursor_char: term.grid()[content.cursor.point].c,
+        }
     }
 
     pub fn focus_in(&self) {
-        if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
+        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
             self.write_to_pty("\x1b[I".to_string());
         }
     }
 
     pub fn focus_out(&self) {
-        if self.last_mode.contains(TermMode::FOCUS_IN_OUT) {
+        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
             self.write_to_pty("\x1b[O".to_string());
         }
     }
 
-    pub fn mouse_changed(&mut self, point: Point, side: Direction) -> bool {
+    pub fn mouse_changed(&mut self, point: Point, side: AlacDirection) -> bool {
         match self.last_mouse {
             Some((old_point, old_side)) => {
                 if old_point == point && old_side == side {
@@ -655,17 +770,17 @@ impl Terminal {
     }
 
     pub fn mouse_mode(&self, shift: bool) -> bool {
-        self.last_mode.intersects(TermMode::MOUSE_MODE) && !shift
+        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
     }
 
     pub fn mouse_move(&mut self, e: &MouseMovedEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
 
-        let point = mouse_point(position, self.cur_size, self.last_offset);
+        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
         let side = mouse_side(position, self.cur_size);
 
         if self.mouse_changed(point, side) && self.mouse_mode(e.shift) {
-            if let Some(bytes) = mouse_moved_report(point, e, self.last_mode) {
+            if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
                 self.pty_tx.notify(bytes);
             }
         }
@@ -677,40 +792,54 @@ impl Terminal {
         if !self.mouse_mode(e.shift) {
             // Alacritty has the same ordering, of first updating the selection
             // then scrolling 15ms later
-            self.continue_selection(position);
+            self.events
+                .push_back(InternalEvent::UpdateSelection(position));
 
             // Doesn't make sense to scroll the alt screen
-            if !self.last_mode.contains(TermMode::ALT_SCREEN) {
-                //TODO: Why do these need to be doubled?
-                let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
-                let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
-
-                let scroll_delta = if e.position.y() < top {
-                    (top - e.position.y()).powf(1.1)
-                } else if e.position.y() > bottom {
-                    -((e.position.y() - bottom).powf(1.1))
-                } else {
-                    return; //Nothing to do
+            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
+                let scroll_delta = match self.drag_line_delta(e) {
+                    Some(value) => value,
+                    None => return,
                 };
 
                 let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32;
-                self.scroll(Scroll::Delta(scroll_lines));
-                self.continue_selection(position)
+
+                self.events
+                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
+                self.events
+                    .push_back(InternalEvent::UpdateSelection(position))
             }
         }
     }
 
+    fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option<f32> {
+        //TODO: Why do these need to be doubled? Probably the same problem that the IME has
+        let top = e.region.origin_y() + (self.cur_size.line_height * 2.);
+        let bottom = e.region.lower_left().y() - (self.cur_size.line_height * 2.);
+        let scroll_delta = if e.position.y() < top {
+            (top - e.position.y()).powf(1.1)
+        } else if e.position.y() > bottom {
+            -((e.position.y() - bottom).powf(1.1))
+        } else {
+            return None; //Nothing to do
+        };
+        Some(scroll_delta)
+    }
+
     pub fn mouse_down(&mut self, e: &DownRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
-        let point = mouse_point(position, self.cur_size, self.last_offset);
+        let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
         let side = mouse_side(position, self.cur_size);
 
         if self.mouse_mode(e.shift) {
-            if let Some(bytes) = mouse_button_report(point, e, true, self.last_mode) {
+            if let Some(bytes) = mouse_button_report(point, e, true, self.last_content.mode) {
                 self.pty_tx.notify(bytes);
             }
         } else if e.button == MouseButton::Left {
-            self.begin_select(Selection::new(SelectionType::Simple, point, side));
+            self.events.push_back(InternalEvent::SetSelection(Some((
+                Selection::new(SelectionType::Simple, point, side),
+                point,
+            ))));
         }
     }
 
@@ -718,7 +847,7 @@ impl Terminal {
         let position = e.position.sub(origin);
 
         if !self.mouse_mode(e.shift) {
-            let point = mouse_point(position, self.cur_size, self.last_offset);
+            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
             let side = mouse_side(position, self.cur_size);
 
             let selection_type = match e.click_count {
@@ -733,7 +862,8 @@ impl Terminal {
                 selection_type.map(|selection_type| Selection::new(selection_type, point, side));
 
             if let Some(sel) = selection {
-                self.begin_select(sel);
+                self.events
+                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
             }
         }
     }
@@ -741,9 +871,9 @@ impl Terminal {
     pub fn mouse_up(&mut self, e: &UpRegionEvent, origin: Vector2F) {
         let position = e.position.sub(origin);
         if self.mouse_mode(e.shift) {
-            let point = mouse_point(position, self.cur_size, self.last_offset);
+            let point = mouse_point(position, self.cur_size, self.last_content.display_offset);
 
-            if let Some(bytes) = mouse_button_report(point, e, false, self.last_mode) {
+            if let Some(bytes) = mouse_button_report(point, e, false, self.last_content.mode) {
                 self.pty_tx.notify(bytes);
             }
         } else if e.button == MouseButton::Left {
@@ -751,6 +881,7 @@ impl Terminal {
             // so let's do that here
             self.copy();
         }
+        self.last_mouse = None;
     }
 
     ///Scroll the terminal
@@ -761,15 +892,22 @@ impl Terminal {
             //The scroll enters 'TouchPhase::Started'. Do I need to replicate this?
             //This would be consistent with a scroll model based on 'distance from origin'...
             let scroll_lines = (e.delta.y() / self.cur_size.line_height) as i32;
-            let point = mouse_point(e.position.sub(origin), self.cur_size, self.last_offset);
-
-            if let Some(scrolls) = scroll_report(point, scroll_lines as i32, e, self.last_mode) {
+            let point = mouse_point(
+                e.position.sub(origin),
+                self.cur_size,
+                self.last_content.display_offset,
+            );
+
+            if let Some(scrolls) =
+                scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
+            {
                 for scroll in scrolls {
                     self.pty_tx.notify(scroll);
                 }
             };
         } else if self
-            .last_mode
+            .last_content
+            .mode
             .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
             && !e.shift
         {
@@ -782,11 +920,39 @@ impl Terminal {
             let scroll_lines =
                 ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32;
             if scroll_lines != 0 {
-                let scroll = Scroll::Delta(scroll_lines);
-                self.scroll(scroll);
+                let scroll = AlacScroll::Delta(scroll_lines);
+
+                self.events.push_back(InternalEvent::Scroll(scroll));
             }
         }
     }
+
+    pub fn find_matches(
+        &mut self,
+        query: project::search::SearchQuery,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Vec<RangeInclusive<Point>>> {
+        let term = self.term.clone();
+        cx.background().spawn(async move {
+            let searcher = match query {
+                project::search::SearchQuery::Text { query, .. } => {
+                    RegexSearch::new(query.as_ref())
+                }
+                project::search::SearchQuery::Regex { query, .. } => {
+                    RegexSearch::new(query.as_ref())
+                }
+            };
+
+            if searcher.is_err() {
+                return Vec::new();
+            }
+            let searcher = searcher.unwrap();
+
+            let term = term.lock();
+
+            make_search_matches(&term, &searcher).collect()
+        })
+    }
 }
 
 impl Drop for Terminal {
@@ -799,6 +965,30 @@ impl Entity for Terminal {
     type Event = Event;
 }
 
+fn make_selection(range: &RangeInclusive<Point>) -> Selection {
+    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
+    selection.update(*range.end(), AlacDirection::Right);
+    selection
+}
+
+/// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches()
+/// Iterate over all visible regex matches.
+fn make_search_matches<'a, T>(
+    term: &'a Term<T>,
+    regex: &'a RegexSearch,
+) -> impl Iterator<Item = Match> + 'a {
+    let viewport_start = Line(-(term.grid().display_offset() as i32));
+    let viewport_end = viewport_start + term.bottommost_line();
+    let mut start = term.line_search_left(Point::new(viewport_start, Column(0)));
+    let mut end = term.line_search_right(Point::new(viewport_end, Column(0)));
+    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
+    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
+
+    RegexIter::new(start, end, AlacDirection::Right, term, regex)
+        .skip_while(move |rm| rm.end().line < viewport_start)
+        .take_while(move |rm| rm.start().line <= viewport_end)
+}
+
 #[cfg(test)]
 mod tests {
     pub mod terminal_test_context;

crates/terminal/src/terminal_container_view.rs πŸ”—

@@ -1,17 +1,20 @@
 use crate::terminal_view::TerminalView;
 use crate::{Event, Terminal, TerminalBuilder, TerminalError};
 
+use alacritty_terminal::index::Point;
 use dirs::home_dir;
 use gpui::{
-    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, View,
-    ViewContext, ViewHandle,
+    actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task,
+    View, ViewContext, ViewHandle,
 };
+use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle};
 use workspace::{Item, Workspace};
 
 use crate::TerminalSize;
 use project::{LocalWorktree, Project, ProjectPath};
 use settings::{AlternateScroll, Settings, WorkingDirectory};
 use smallvec::SmallVec;
+use std::ops::RangeInclusive;
 use std::path::{Path, PathBuf};
 
 use crate::terminal_element::TerminalElement;
@@ -26,12 +29,12 @@ pub fn init(cx: &mut MutableAppContext) {
 //Take away all the result unwrapping in the current TerminalView by making it 'infallible'
 //Bubble up to deploy(_modal)() calls
 
-pub enum TerminalContent {
+pub enum TerminalContainerContent {
     Connected(ViewHandle<TerminalView>),
     Error(ViewHandle<ErrorView>),
 }
 
-impl TerminalContent {
+impl TerminalContainerContent {
     fn handle(&self) -> AnyViewHandle {
         match self {
             Self::Connected(handle) => handle.into(),
@@ -42,7 +45,7 @@ impl TerminalContent {
 
 pub struct TerminalContainer {
     modal: bool,
-    pub content: TerminalContent,
+    pub content: TerminalContainerContent,
     associated_directory: Option<PathBuf>,
 }
 
@@ -116,13 +119,13 @@ impl TerminalContainer {
                 let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
                 cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
                     .detach();
-                TerminalContent::Connected(view)
+                TerminalContainerContent::Connected(view)
             }
             Err(error) => {
                 let view = cx.add_view(|_| ErrorView {
                     error: error.downcast::<TerminalError>().unwrap(),
                 });
-                TerminalContent::Error(view)
+                TerminalContainerContent::Error(view)
             }
         };
         cx.focus(content.handle());
@@ -142,7 +145,7 @@ impl TerminalContainer {
         let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
         TerminalContainer {
             modal,
-            content: TerminalContent::Connected(connected_view),
+            content: TerminalContainerContent::Connected(connected_view),
             associated_directory: None,
         }
     }
@@ -155,8 +158,8 @@ impl View for TerminalContainer {
 
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
         let child_view = match &self.content {
-            TerminalContent::Connected(connected) => ChildView::new(connected),
-            TerminalContent::Error(error) => ChildView::new(error),
+            TerminalContainerContent::Connected(connected) => ChildView::new(connected),
+            TerminalContainerContent::Error(error) => ChildView::new(error),
         };
         if self.modal {
             let settings = cx.global::<Settings>();
@@ -235,10 +238,10 @@ impl Item for TerminalContainer {
         cx: &gpui::AppContext,
     ) -> ElementBox {
         let title = match &self.content {
-            TerminalContent::Connected(connected) => {
+            TerminalContainerContent::Connected(connected) => {
                 connected.read(cx).handle().read(cx).title.to_string()
             }
-            TerminalContent::Error(_) => "Terminal".to_string(),
+            TerminalContainerContent::Error(_) => "Terminal".to_string(),
         };
 
         Flex::row()
@@ -306,7 +309,7 @@ impl Item for TerminalContainer {
     }
 
     fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
-        if let TerminalContent::Connected(connected) = &self.content {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
             connected.read(cx).has_new_content()
         } else {
             false
@@ -314,7 +317,7 @@ impl Item for TerminalContainer {
     }
 
     fn has_conflict(&self, cx: &AppContext) -> bool {
-        if let TerminalContent::Connected(connected) = &self.content {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
             connected.read(cx).has_bell()
         } else {
             false
@@ -328,6 +331,115 @@ impl Item for TerminalContainer {
     fn should_close_item_on_event(event: &Self::Event) -> bool {
         matches!(event, &Event::CloseTerminal)
     }
+
+    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+        Some(Box::new(handle.clone()))
+    }
+}
+
+impl SearchableItem for TerminalContainer {
+    type Match = RangeInclusive<Point>;
+
+    fn supported_options() -> SearchOptions {
+        SearchOptions {
+            case: false,
+            word: false,
+            regex: false,
+        }
+    }
+
+    /// Convert events raised by this item into search-relevant events (if applicable)
+    fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
+        match event {
+            Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
+            Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
+            _ => None,
+        }
+    }
+
+    /// Clear stored matches
+    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            let terminal = connected.read(cx).terminal().clone();
+            terminal.update(cx, |term, _| term.matches.clear())
+        }
+    }
+
+    /// Store matches returned from find_matches somewhere for rendering
+    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            let terminal = connected.read(cx).terminal().clone();
+            terminal.update(cx, |term, _| term.matches = matches)
+        }
+    }
+
+    /// Return the selection content to pre-load into this search
+    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            let terminal = connected.read(cx).terminal().clone();
+            terminal
+                .read(cx)
+                .last_content
+                .selection_text
+                .clone()
+                .unwrap_or_default()
+        } else {
+            Default::default()
+        }
+    }
+
+    /// Focus match at given index into the Vec of matches
+    fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            let terminal = connected.read(cx).terminal().clone();
+            terminal.update(cx, |term, _| term.activate_match(index));
+            cx.notify();
+        }
+    }
+
+    /// Get all of the matches for this query, should be done on the background
+    fn find_matches(
+        &mut self,
+        query: project::search::SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Vec<Self::Match>> {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            let terminal = connected.read(cx).terminal().clone();
+            terminal.update(cx, |term, cx| term.find_matches(query, cx))
+        } else {
+            Task::ready(Vec::new())
+        }
+    }
+
+    /// Reports back to the search toolbar what the active match should be (the selection)
+    fn active_match_index(
+        &mut self,
+        matches: Vec<Self::Match>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<usize> {
+        if let TerminalContainerContent::Connected(connected) = &self.content {
+            if let Some(selection_head) = connected.read(cx).terminal().read(cx).selection_head {
+                // If selection head is contained in a match. Return that match
+                for (ix, search_match) in matches.iter().enumerate() {
+                    if search_match.contains(&selection_head) {
+                        return Some(ix);
+                    }
+
+                    // If not contained, return the next match after the selection head
+                    if search_match.start() > &selection_head {
+                        return Some(ix);
+                    }
+                }
+
+                // If no selection after selection head, return the last match
+                return Some(matches.len() - 1);
+            } else {
+                Some(0)
+            }
+        } else {
+            None
+        }
+    }
 }
 
 ///Get's the working directory for the given workspace, respecting the user's settings.

crates/terminal/src/terminal_element.rs πŸ”—

@@ -2,11 +2,7 @@ use alacritty_terminal::{
     ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
     grid::Dimensions,
     index::Point,
-    selection::SelectionRange,
-    term::{
-        cell::{Cell, Flags},
-        TermMode,
-    },
+    term::{cell::Flags, TermMode},
 };
 use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine};
 use gpui::{
@@ -27,43 +23,25 @@ use settings::Settings;
 use theme::TerminalStyle;
 use util::ResultExt;
 
-use std::fmt::Debug;
-use std::{
-    mem,
-    ops::{Deref, Range},
-};
+use std::{fmt::Debug, ops::RangeInclusive};
+use std::{mem, ops::Range};
 
 use crate::{
     mappings::colors::convert_color,
     terminal_view::{DeployContextMenu, TerminalView},
-    Terminal, TerminalSize,
+    IndexedCell, Terminal, TerminalContent, TerminalSize,
 };
 
 ///The information generated during layout that is nescessary for painting
 pub struct LayoutState {
     cells: Vec<LayoutCell>,
     rects: Vec<LayoutRect>,
-    highlights: Vec<RelativeHighlightedRange>,
+    relative_highlighted_ranges: Vec<(RangeInclusive<Point>, Color)>,
     cursor: Option<Cursor>,
     background_color: Color,
-    selection_color: Color,
     size: TerminalSize,
     mode: TermMode,
-}
-
-#[derive(Debug)]
-struct IndexedCell {
-    point: Point,
-    cell: Cell,
-}
-
-impl Deref for IndexedCell {
-    type Target = Cell;
-
-    #[inline]
-    fn deref(&self) -> &Cell {
-        &self.cell
-    }
+    display_offset: usize,
 }
 
 ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points
@@ -166,30 +144,6 @@ impl LayoutRect {
     }
 }
 
-#[derive(Clone, Debug, Default)]
-struct RelativeHighlightedRange {
-    line_index: usize,
-    range: Range<usize>,
-}
-
-impl RelativeHighlightedRange {
-    fn new(line_index: usize, range: Range<usize>) -> Self {
-        RelativeHighlightedRange { line_index, range }
-    }
-
-    fn to_highlighted_range_line(
-        &self,
-        origin: Vector2F,
-        layout: &LayoutState,
-    ) -> HighlightedRangeLine {
-        let start_x = origin.x() + self.range.start as f32 * layout.size.cell_width;
-        let end_x =
-            origin.x() + self.range.end as f32 * layout.size.cell_width + layout.size.cell_width;
-
-        HighlightedRangeLine { start_x, end_x }
-    }
-}
-
 ///The GPUI element that paints the terminal.
 ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
 pub struct TerminalElement {
@@ -217,48 +171,31 @@ impl TerminalElement {
         }
     }
 
+    //Vec<Range<Point>> -> Clip out the parts of the ranges
+
     fn layout_grid(
-        grid: Vec<IndexedCell>,
+        grid: &Vec<IndexedCell>,
         text_style: &TextStyle,
         terminal_theme: &TerminalStyle,
         text_layout_cache: &TextLayoutCache,
         font_cache: &FontCache,
         modal: bool,
-        selection_range: Option<SelectionRange>,
-    ) -> (
-        Vec<LayoutCell>,
-        Vec<LayoutRect>,
-        Vec<RelativeHighlightedRange>,
-    ) {
+    ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
         let mut cells = vec![];
         let mut rects = vec![];
-        let mut highlight_ranges = vec![];
 
         let mut cur_rect: Option<LayoutRect> = None;
         let mut cur_alac_color = None;
-        let mut highlighted_range = None;
 
         let linegroups = grid.into_iter().group_by(|i| i.point.line);
         for (line_index, (_, line)) in linegroups.into_iter().enumerate() {
-            for (x_index, cell) in line.enumerate() {
+            for cell in line {
                 let mut fg = cell.fg;
                 let mut bg = cell.bg;
                 if cell.flags.contains(Flags::INVERSE) {
                     mem::swap(&mut fg, &mut bg);
                 }
 
-                //Increase selection range
-                {
-                    if selection_range
-                        .map(|range| range.contains(cell.point))
-                        .unwrap_or(false)
-                    {
-                        let mut range = highlighted_range.take().unwrap_or(x_index..x_index);
-                        range.end = range.end.max(x_index);
-                        highlighted_range = Some(range);
-                    }
-                }
-
                 //Expand background rect range
                 {
                     if matches!(bg, Named(NamedColor::Background)) {
@@ -324,18 +261,11 @@ impl TerminalElement {
                 };
             }
 
-            if highlighted_range.is_some() {
-                highlight_ranges.push(RelativeHighlightedRange::new(
-                    line_index,
-                    highlighted_range.take().unwrap(),
-                ))
-            }
-
             if cur_rect.is_some() {
                 rects.push(cur_rect.take().unwrap());
             }
         }
-        (cells, rects, highlight_ranges)
+        (cells, rects)
     }
 
     // Compute the cursor position and expected block width, may return a zero width if x_for_index returns
@@ -612,60 +542,59 @@ impl Element for TerminalElement {
         let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
         let text_style = TerminalElement::make_text_style(font_cache, settings);
         let selection_color = settings.theme.editor.selection.selection;
+        let match_color = settings.theme.search.match_background;
         let dimensions = {
             let line_height = font_cache.line_height(text_style.font_size);
             let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
             TerminalSize::new(line_height, cell_width, constraint.max)
         };
 
+        let search_matches = if let Some(terminal_model) = self.terminal.upgrade(cx) {
+            terminal_model.read(cx).matches.clone()
+        } else {
+            Default::default()
+        };
+
         let background_color = if self.modal {
             terminal_theme.colors.modal_background
         } else {
             terminal_theme.colors.background
         };
+        let terminal_handle = self.terminal.upgrade(cx).unwrap();
 
-        let (cells, selection, cursor, display_offset, cursor_text, mode) = self
-            .terminal
-            .upgrade(cx)
-            .unwrap()
-            .update(cx.app, |terminal, mcx| {
-                terminal.set_size(dimensions);
-                terminal.render_lock(mcx, |content, cursor_text| {
-                    let mut cells = vec![];
-                    cells.extend(
-                        content
-                            .display_iter
-                            //TODO: Add this once there's a way to retain empty lines
-                            // .filter(|ic| {
-                            //     !ic.flags.contains(Flags::HIDDEN)
-                            //         && !(ic.bg == Named(NamedColor::Background)
-                            //             && ic.c == ' '
-                            //             && !ic.flags.contains(Flags::INVERSE))
-                            // })
-                            .map(|ic| IndexedCell {
-                                point: ic.point,
-                                cell: ic.cell.clone(),
-                            }),
-                    );
-                    (
-                        cells,
-                        content.selection,
-                        content.cursor,
-                        content.display_offset,
-                        cursor_text,
-                        content.mode,
-                    )
-                })
-            });
+        terminal_handle.update(cx.app, |terminal, cx| {
+            terminal.set_size(dimensions);
+            terminal.try_sync(cx)
+        });
+
+        let TerminalContent {
+            cells,
+            mode,
+            display_offset,
+            cursor_char,
+            selection,
+            cursor,
+            ..
+        } = &terminal_handle.read(cx).last_content;
+
+        // searches, highlights to a single range representations
+        let mut relative_highlighted_ranges = Vec::new();
+        for search_match in search_matches {
+            relative_highlighted_ranges.push((search_match, match_color))
+        }
+        if let Some(selection) = selection {
+            relative_highlighted_ranges.push((selection.start..=selection.end, selection_color));
+        }
+
+        // then have that representation be converted to the appropriate highlight data structure
 
-        let (cells, rects, highlights) = TerminalElement::layout_grid(
+        let (cells, rects) = TerminalElement::layout_grid(
             cells,
             &text_style,
             &terminal_theme,
             cx.text_layout_cache,
             cx.font_cache(),
             self.modal,
-            selection,
         );
 
         //Layout cursor. Rectangle is used for IME, so we should lay it out even
@@ -673,9 +602,9 @@ impl Element for TerminalElement {
         let cursor = if let AlacCursorShape::Hidden = cursor.shape {
             None
         } else {
-            let cursor_point = DisplayCursor::from(cursor.point, display_offset);
+            let cursor_point = DisplayCursor::from(cursor.point, *display_offset);
             let cursor_text = {
-                let str_trxt = cursor_text.to_string();
+                let str_trxt = cursor_char.to_string();
 
                 let color = if self.focused {
                     terminal_theme.colors.background
@@ -728,11 +657,11 @@ impl Element for TerminalElement {
                 cells,
                 cursor,
                 background_color,
-                selection_color,
                 size: dimensions,
                 rects,
-                highlights,
-                mode,
+                relative_highlighted_ranges,
+                mode: *mode,
+                display_offset: *display_offset,
             },
         )
     }
@@ -753,6 +682,11 @@ impl Element for TerminalElement {
             //Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
             self.attach_mouse_handlers(origin, self.view.id(), visible_bounds, layout.mode, cx);
 
+            cx.scene.push_cursor_region(gpui::CursorRegion {
+                bounds,
+                style: gpui::CursorStyle::IBeam,
+            });
+
             cx.paint_layer(clip_bounds, |cx| {
                 //Start with a background color
                 cx.scene.push_quad(Quad {
@@ -767,30 +701,23 @@ impl Element for TerminalElement {
                 }
             });
 
-            //Draw Selection
+            //Draw Highlighted Backgrounds
             cx.paint_layer(clip_bounds, |cx| {
-                let start_y = layout.highlights.get(0).map(|highlight| {
-                    origin.y() + highlight.line_index as f32 * layout.size.line_height
-                });
-
-                if let Some(y) = start_y {
-                    let range_lines = layout
-                        .highlights
-                        .iter()
-                        .map(|relative_highlight| {
-                            relative_highlight.to_highlighted_range_line(origin, layout)
-                        })
-                        .collect::<Vec<HighlightedRangeLine>>();
-
-                    let hr = HighlightedRange {
-                        start_y: y, //Need to change this
-                        line_height: layout.size.line_height,
-                        lines: range_lines,
-                        color: layout.selection_color,
-                        //Copied from editor. TODO: move to theme or something
-                        corner_radius: 0.15 * layout.size.line_height,
-                    };
-                    hr.paint(bounds, cx.scene);
+                for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
+                {
+                    if let Some((start_y, highlighted_range_lines)) =
+                        to_highlighted_range_lines(relative_highlighted_range, layout, origin)
+                    {
+                        let hr = HighlightedRange {
+                            start_y, //Need to change this
+                            line_height: layout.size.line_height,
+                            lines: highlighted_range_lines,
+                            color: color.clone(),
+                            //Copied from editor. TODO: move to theme or something
+                            corner_radius: 0.15 * layout.size.line_height,
+                        };
+                        hr.paint(bounds, cx.scene);
+                    }
                 }
             });
 
@@ -893,3 +820,65 @@ impl Element for TerminalElement {
         Some(layout.cursor.as_ref()?.bounding_rect(origin))
     }
 }
+
+fn to_highlighted_range_lines(
+    range: &RangeInclusive<Point>,
+    layout: &LayoutState,
+    origin: Vector2F,
+) -> Option<(f32, Vec<HighlightedRangeLine>)> {
+    // Step 1. Normalize the points to be viewport relative.
+    //When display_offset = 1, here's how the grid is arranged:
+    //--- Viewport top
+    //-2,0 -2,1...
+    //-1,0 -1,1...
+    //--------- Terminal Top
+    // 0,0  0,1...
+    // 1,0  1,1...
+    //--- Viewport Bottom
+    // 2,0  2,1...
+    //--------- Terminal Bottom
+
+    // Normalize to viewport relative, from terminal relative.
+    // lines are i32s, which are negative above the top left corner of the terminal
+    // If the user has scrolled, we use the display_offset to tell us which offset
+    // of the grid data we should be looking at. But for the rendering step, we don't
+    // want negatives. We want things relative to the 'viewport' (the area of the grid
+    // which is currently shown according to the display offset)
+    let unclamped_start = Point::new(
+        range.start().line + layout.display_offset,
+        range.start().column,
+    );
+    let unclamped_end = Point::new(range.end().line + layout.display_offset, range.end().column);
+
+    // Step 2. Clamp range to viewport, and return None if it doesn't overlap
+    if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 {
+        return None;
+    }
+
+    let clamped_start_line = unclamped_start.line.0.max(0) as usize;
+    let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize;
+    //Convert the start of the range to pixels
+    let start_y = origin.y() + clamped_start_line as f32 * layout.size.line_height;
+
+    // Step 3. Expand ranges that cross lines into a collection of single-line ranges.
+    //  (also convert to pixels)
+    let mut highlighted_range_lines = Vec::new();
+    for line in clamped_start_line..=clamped_end_line {
+        let mut line_start = 0;
+        let mut line_end = layout.size.columns();
+
+        if line == clamped_start_line {
+            line_start = unclamped_start.column.0 as usize;
+        }
+        if line == clamped_end_line {
+            line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive
+        }
+
+        highlighted_range_lines.push(HighlightedRangeLine {
+            start_x: origin.x() + line_start as f32 * layout.size.cell_width,
+            end_x: origin.x() + line_end as f32 * layout.size.cell_width,
+        });
+    }
+
+    Some((start_y, highlighted_range_lines))
+}

crates/terminal/src/terminal_view.rs πŸ”—

@@ -1,6 +1,6 @@
-use std::time::Duration;
+use std::{ops::RangeInclusive, time::Duration};
 
-use alacritty_terminal::term::TermMode;
+use alacritty_terminal::{index::Point, term::TermMode};
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     actions,
@@ -8,8 +8,8 @@ use gpui::{
     geometry::vector::Vector2F,
     impl_internal_actions,
     keymap::Keystroke,
-    AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View,
-    ViewContext, ViewHandle,
+    AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
+    View, ViewContext, ViewHandle,
 };
 use settings::{Settings, TerminalBlink};
 use smol::Timer;
@@ -40,6 +40,7 @@ actions!(
         Copy,
         Paste,
         ShowCharacterPalette,
+        SearchTest
     ]
 );
 impl_internal_actions!(project_panel, [DeployContextMenu]);
@@ -148,7 +149,8 @@ impl TerminalView {
         if !self
             .terminal
             .read(cx)
-            .last_mode
+            .last_content
+            .mode
             .contains(TermMode::ALT_SCREEN)
         {
             cx.show_character_palette();
@@ -176,7 +178,8 @@ impl TerminalView {
             || self
                 .terminal
                 .read(cx)
-                .last_mode
+                .last_content
+                .mode
                 .contains(TermMode::ALT_SCREEN)
         {
             return true;
@@ -235,6 +238,19 @@ impl TerminalView {
         .detach();
     }
 
+    pub fn find_matches(
+        &mut self,
+        query: project::search::SearchQuery,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Vec<RangeInclusive<Point>>> {
+        self.terminal
+            .update(cx, |term, cx| term.find_matches(query, cx))
+    }
+
+    pub fn terminal(&self) -> &ModelHandle<Terminal> {
+        &self.terminal
+    }
+
     fn next_blink_epoch(&mut self) -> usize {
         self.blink_epoch += 1;
         self.blink_epoch
@@ -348,7 +364,8 @@ impl View for TerminalView {
         if self
             .terminal
             .read(cx)
-            .last_mode
+            .last_content
+            .mode
             .contains(TermMode::ALT_SCREEN)
         {
             None
@@ -373,7 +390,7 @@ impl View for TerminalView {
         if self.modal {
             context.set.insert("ModalTerminal".into());
         }
-        let mode = self.terminal.read(cx).last_mode;
+        let mode = self.terminal.read(cx).last_content.mode;
         context.map.insert(
             "screen".to_string(),
             (if mode.contains(TermMode::ALT_SCREEN) {

crates/workspace/src/searchable.rs πŸ”—

@@ -10,38 +10,73 @@ use crate::{Item, ItemHandle, WeakItemHandle};
 
 #[derive(Debug)]
 pub enum SearchEvent {
-    ContentsUpdated,
-    SelectionsChanged,
+    MatchesInvalidated,
+    ActiveMatchChanged,
 }
 
-#[derive(Clone, Copy, PartialEq, Eq)]
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
 pub enum Direction {
     Prev,
     Next,
 }
 
+#[derive(Clone, Copy, Debug, Default)]
+pub struct SearchOptions {
+    pub case: bool,
+    pub word: bool,
+    pub regex: bool,
+}
+
 pub trait SearchableItem: Item {
     type Match: Any + Sync + Send + Clone;
 
+    fn supported_options() -> SearchOptions {
+        SearchOptions {
+            case: true,
+            word: true,
+            regex: true,
+        }
+    }
     fn to_search_event(event: &Self::Event) -> Option<SearchEvent>;
-    fn clear_highlights(&mut self, cx: &mut ViewContext<Self>);
-    fn highlight_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
+    fn clear_matches(&mut self, cx: &mut ViewContext<Self>);
+    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>);
     fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String;
-    fn select_next_match_in_direction(
+    fn activate_match(
         &mut self,
         index: usize,
-        direction: Direction,
         matches: Vec<Self::Match>,
         cx: &mut ViewContext<Self>,
     );
-    fn select_match_by_index(
+    fn match_index_for_direction(
         &mut self,
-        index: usize,
-        matches: Vec<Self::Match>,
+        matches: &Vec<Self::Match>,
+        mut current_index: usize,
+        direction: Direction,
+        _: &mut ViewContext<Self>,
+    ) -> usize {
+        match direction {
+            Direction::Prev => {
+                if current_index == 0 {
+                    matches.len() - 1
+                } else {
+                    current_index - 1
+                }
+            }
+            Direction::Next => {
+                current_index += 1;
+                if current_index == matches.len() {
+                    0
+                } else {
+                    current_index
+                }
+            }
+        }
+    }
+    fn find_matches(
+        &mut self,
+        query: SearchQuery,
         cx: &mut ViewContext<Self>,
-    );
-    fn matches(&mut self, query: SearchQuery, cx: &mut ViewContext<Self>)
-        -> Task<Vec<Self::Match>>;
+    ) -> Task<Vec<Self::Match>>;
     fn active_match_index(
         &mut self,
         matches: Vec<Self::Match>,
@@ -52,28 +87,29 @@ pub trait SearchableItem: Item {
 pub trait SearchableItemHandle: ItemHandle {
     fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle>;
     fn boxed_clone(&self) -> Box<dyn SearchableItemHandle>;
+    fn supported_options(&self) -> SearchOptions;
     fn subscribe(
         &self,
         cx: &mut MutableAppContext,
         handler: Box<dyn Fn(SearchEvent, &mut MutableAppContext)>,
     ) -> Subscription;
-    fn clear_highlights(&self, cx: &mut MutableAppContext);
-    fn highlight_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext);
+    fn clear_matches(&self, cx: &mut MutableAppContext);
+    fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext);
     fn query_suggestion(&self, cx: &mut MutableAppContext) -> String;
-    fn select_next_match_in_direction(
+    fn activate_match(
         &self,
         index: usize,
-        direction: Direction,
         matches: &Vec<Box<dyn Any + Send>>,
         cx: &mut MutableAppContext,
     );
-    fn select_match_by_index(
+    fn match_index_for_direction(
         &self,
-        index: usize,
         matches: &Vec<Box<dyn Any + Send>>,
+        current_index: usize,
+        direction: Direction,
         cx: &mut MutableAppContext,
-    );
-    fn matches(
+    ) -> usize;
+    fn find_matches(
         &self,
         query: SearchQuery,
         cx: &mut MutableAppContext,
@@ -94,6 +130,10 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
+    fn supported_options(&self) -> SearchOptions {
+        T::supported_options()
+    }
+
     fn subscribe(
         &self,
         cx: &mut MutableAppContext,
@@ -106,45 +146,43 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
         })
     }
 
-    fn clear_highlights(&self, cx: &mut MutableAppContext) {
-        self.update(cx, |this, cx| this.clear_highlights(cx));
+    fn clear_matches(&self, cx: &mut MutableAppContext) {
+        self.update(cx, |this, cx| this.clear_matches(cx));
     }
-    fn highlight_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext) {
+    fn update_matches(&self, matches: &Vec<Box<dyn Any + Send>>, cx: &mut MutableAppContext) {
         let matches = downcast_matches(matches);
-        self.update(cx, |this, cx| this.highlight_matches(matches, cx));
+        self.update(cx, |this, cx| this.update_matches(matches, cx));
     }
     fn query_suggestion(&self, cx: &mut MutableAppContext) -> String {
         self.update(cx, |this, cx| this.query_suggestion(cx))
     }
-    fn select_next_match_in_direction(
+    fn activate_match(
         &self,
         index: usize,
-        direction: Direction,
         matches: &Vec<Box<dyn Any + Send>>,
         cx: &mut MutableAppContext,
     ) {
         let matches = downcast_matches(matches);
-        self.update(cx, |this, cx| {
-            this.select_next_match_in_direction(index, direction, matches, cx)
-        });
+        self.update(cx, |this, cx| this.activate_match(index, matches, cx));
     }
-    fn select_match_by_index(
+    fn match_index_for_direction(
         &self,
-        index: usize,
         matches: &Vec<Box<dyn Any + Send>>,
+        current_index: usize,
+        direction: Direction,
         cx: &mut MutableAppContext,
-    ) {
+    ) -> usize {
         let matches = downcast_matches(matches);
         self.update(cx, |this, cx| {
-            this.select_match_by_index(index, matches, cx)
-        });
+            this.match_index_for_direction(&matches, current_index, direction, cx)
+        })
     }
-    fn matches(
+    fn find_matches(
         &self,
         query: SearchQuery,
         cx: &mut MutableAppContext,
     ) -> Task<Vec<Box<dyn Any + Send>>> {
-        let matches = self.update(cx, |this, cx| this.matches(query, cx));
+        let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
         cx.foreground().spawn(async {
             let matches = matches.await;
             matches

styles/package-lock.json πŸ”—

@@ -5,7 +5,6 @@
   "requires": true,
   "packages": {
     "": {
-      "name": "styles",
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {