From f62b69adb4286cfef35e3f738e2f7f2a9fbc7721 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 29 Aug 2022 17:07:31 -0700 Subject: [PATCH 1/8] Checkpoint commit, almost have the initial search research done. Don't forget to remove test keymap --- assets/keymaps/default.json | 3 +- crates/terminal/src/terminal.rs | 111 +++++++++++++++++++++------ crates/terminal/src/terminal_view.rs | 11 +++ 3 files changed, 101 insertions(+), 24 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1d95c33cbf1edc3fc4ee4a8bd1c4a7f8a3118418..2fb1f010a66fc453b425ddfdba0ea5de566305c1 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -424,7 +424,8 @@ "ctrl-cmd-space": "terminal::ShowCharacterPalette", "cmd-c": "terminal::Copy", "cmd-v": "terminal::Paste", - "cmd-k": "terminal::Clear" + "cmd-k": "terminal::Clear", + "cmd-s": "terminal::SearchTest" } }, { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 52af9cc5e808264a4802d5c25fabe187d28c3ca6..6b9ec4f295d684bd726fa49103231f7534a2af72 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -9,11 +9,11 @@ use alacritty_terminal::{ config::{Config, Program, PtyConfig, Scrolling}, event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, - grid::{Dimensions, Scroll}, + grid::{Dimensions, Scroll as AlacScroll}, index::{Direction, Point}, selection::{Selection, SelectionType}, sync::FairMutex, - term::{RenderableContent, TermMode}, + term::{search::RegexSearch, RenderableContent, TermMode}, tty::{self, setup_env}, Term, }; @@ -62,7 +62,9 @@ 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 ALACRITTY_SEARCH_LINE_LIMIT: usize = 1000; +const SEARCH_FORWARD: Direction = Direction::Left; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; @@ -79,6 +81,12 @@ pub enum Event { BlinkChanged, } +#[derive(Clone, Debug)] +enum Scroll { + AlacScroll(AlacScroll), + ToNextSearch, +} + #[derive(Clone, Debug)] enum InternalEvent { TermEvent(AlacTermEvent), @@ -370,7 +378,8 @@ impl TerminalBuilder { cur_size: initial_size, last_mouse: None, last_offset: 0, - current_selection: false, + has_selection: false, + searcher: None, }; Ok(TerminalBuilder { @@ -442,7 +451,8 @@ pub struct Terminal { last_mode: TermMode, last_offset: usize, last_mouse: Option<(Point, Direction)>, - current_selection: bool, + has_selection: bool, + searcher: Option<(Option, Point)>, } impl Terminal { @@ -516,9 +526,20 @@ impl Terminal { self.write_to_pty("\x0c".to_string()); term.clear_screen(ClearMode::Saved); } - InternalEvent::Scroll(scroll) => { + InternalEvent::Scroll(Scroll::AlacScroll(scroll)) => { term.scroll_display(*scroll); } + InternalEvent::Scroll(Scroll::ToNextSearch) => { + if let Some((Some(searcher), origin)) = &self.searcher { + match term.search_next(searcher, *origin, SEARCH_FORWARD, Direction::Left, None) + { + Some(regex_match) => { + //Jump to spot + } + None => { /*reset state*/ } + } + } + } InternalEvent::SetSelection(sel) => term.selection = sel.clone(), InternalEvent::UpdateSelection(position) => { if let Some(mut selection) = term.selection.take() { @@ -539,7 +560,7 @@ impl Terminal { } fn begin_select(&mut self, sel: Selection) { - self.current_selection = true; + self.has_selection = true; self.events .push_back(InternalEvent::SetSelection(Some(sel))); } @@ -550,12 +571,45 @@ impl Terminal { } fn end_select(&mut self) { - self.current_selection = false; + self.has_selection = false; self.events.push_back(InternalEvent::SetSelection(None)); } - fn scroll(&mut self, scroll: Scroll) { - self.events.push_back(InternalEvent::Scroll(scroll)); + fn scroll(&mut self, scroll: AlacScroll) { + self.events + .push_back(InternalEvent::Scroll(Scroll::AlacScroll(scroll))); + } + + fn scroll_to_next_search(&mut self) { + self.events + .push_back(InternalEvent::Scroll(Scroll::ToNextSearch)); + } + + pub fn search(&mut self, search: &str) { + let new_searcher = RegexSearch::new(search).ok(); + self.searcher = match (new_searcher, &self.searcher) { + //Existing search, carry over origin + (Some(new_searcher), Some((_, origin))) => Some((Some(new_searcher), *origin)), + //No existing search, start a new one + (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())), + //Error creating a new search, carry over origin + (None, Some((_, origin))) => Some((None, *origin)), + //Nothing to do :( + (None, None) => None, + }; + + if let Some((Some(_), _)) = self.searcher { + self.scroll_to_next_search(); + } + } + + fn viewport_origin(&mut self) -> Point { + let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1; + Point::new(viewport_top, alacritty_terminal::index::Column(0)) + } + + pub fn end_search(&mut self) { + self.searcher = None; } pub fn copy(&mut self) { @@ -577,7 +631,7 @@ impl Terminal { } pub fn input(&mut self, input: String) { - self.scroll(Scroll::Bottom); + self.scroll(AlacScroll::Bottom); self.end_select(); self.write_to_pty(input); } @@ -618,6 +672,9 @@ impl Terminal { let content = term.renderable_content(); + // term.line_search_right(point) + // term.search_next(dfas, origin, direction, side, max_lines) + self.last_offset = content.display_offset; let cursor_text = term.grid()[content.cursor.point].c; @@ -681,25 +738,32 @@ impl Terminal { // 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 + 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.scroll(AlacScroll::Delta(scroll_lines)); self.continue_selection(position) } } } + fn drag_line_delta(&mut self, e: DragRegionEvent) -> Option { + //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); @@ -751,6 +815,7 @@ impl Terminal { // so let's do that here self.copy(); } + self.last_mouse = None; } ///Scroll the terminal @@ -782,7 +847,7 @@ 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); + let scroll = AlacScroll::Delta(scroll_lines); self.scroll(scroll); } } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index e5117604c5b9c953ea2b62daed94b8e2c0ef12de..0e067ac0fb93dfaf214028621e14baa764982f49 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -40,6 +40,7 @@ actions!( Copy, Paste, ShowCharacterPalette, + SearchTest ] ); impl_internal_actions!(project_panel, [DeployContextMenu]); @@ -57,6 +58,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(TerminalView::paste); cx.add_action(TerminalView::clear); cx.add_action(TerminalView::show_character_palette); + + cx.add_action(TerminalView::test_search); } ///A terminal view, maintains the PTY's file handles and communicates with the terminal @@ -159,6 +162,14 @@ impl TerminalView { } } + fn test_search(&mut self, _: &SearchTest, cx: &mut ViewContext) { + let search_string = "ttys"; + self.terminal.update(cx, |term, _| { + term.search(search_string); + }); + cx.notify(); + } + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { self.terminal.update(cx, |term, _| term.clear()); cx.notify(); From 8e7d9cf22ec718e0484d40caf774aba01ac1c601 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 Aug 2022 12:38:42 -0700 Subject: [PATCH 2/8] search stuff --- crates/diagnostics/src/items.rs | 2 +- crates/terminal/src/search.rs | 78 ++++++++++++++++++ crates/terminal/src/terminal.rs | 100 +++++++++++++----------- crates/terminal/src/terminal_element.rs | 15 ++-- 4 files changed, 140 insertions(+), 55 deletions(-) create mode 100644 crates/terminal/src/search.rs diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index cc5932a781deb100dd5b08be89bbb72e4b196a36..b843726a2e402f2b87822107c5aba3e85d348d2e 100644 --- a/crates/diagnostics/src/items.rs +++ b/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() diff --git a/crates/terminal/src/search.rs b/crates/terminal/src/search.rs new file mode 100644 index 0000000000000000000000000000000000000000..d9d7b076eb9ccb036cca67370222e159ba78f414 --- /dev/null +++ b/crates/terminal/src/search.rs @@ -0,0 +1,78 @@ +use std::{borrow::Cow, ops::Deref}; + +use alacritty_terminal::{ + grid::Dimensions, + index::{Column, Direction, Line, Point}, + term::search::{Match, RegexIter, RegexSearch}, + Term, +}; + +const MAX_SEARCH_LINES: usize = 100; + +///Header and impl fom alacritty/src/display/content.rs HintMatches +#[derive(Default)] +pub struct SearchMatches<'a> { + /// All visible matches. + matches: Cow<'a, [Match]>, + + /// Index of the last match checked. + index: usize, +} + +impl<'a> SearchMatches<'a> { + /// Create new renderable matches iterator.. + fn new(matches: impl Into>) -> Self { + Self { + matches: matches.into(), + index: 0, + } + } + + /// Create from regex matches on term visable part. + pub fn visible_regex_matches(term: &Term, dfas: &RegexSearch) -> Self { + let matches = visible_regex_match_iter(term, dfas).collect::>(); + Self::new(matches) + } + + /// Advance the regex tracker to the next point. + /// + /// This will return `true` if the point passed is part of a regex match. + fn advance(&mut self, point: Point) -> bool { + while let Some(bounds) = self.get(self.index) { + if bounds.start() > &point { + break; + } else if bounds.end() < &point { + self.index += 1; + } else { + return true; + } + } + false + } +} + +impl<'a> Deref for SearchMatches<'a> { + type Target = [Match]; + + fn deref(&self) -> &Self::Target { + self.matches.deref() + } +} + +/// Copied from alacritty/src/display/hint.rs +/// Iterate over all visible regex matches. +fn visible_regex_match_iter<'a, T>( + term: &'a Term, + regex: &'a RegexSearch, +) -> impl Iterator + '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, Direction::Right, term, regex) + .skip_while(move |rm| rm.end().line < viewport_start) + .take_while(move |rm| rm.start().line <= viewport_end) +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 6b9ec4f295d684bd726fa49103231f7534a2af72..3b40452ee6fe3ddc6aa1b716a5708eafc04b5b97 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,6 @@ pub mod mappings; pub mod modal; +pub mod search; pub mod terminal_container_view; pub mod terminal_element; pub mod terminal_view; @@ -13,7 +14,7 @@ use alacritty_terminal::{ index::{Direction, Point}, selection::{Selection, SelectionType}, sync::FairMutex, - term::{search::RegexSearch, RenderableContent, TermMode}, + term::{color::Rgb, search::RegexSearch, RenderableContent, TermMode}, tty::{self, setup_env}, Term, }; @@ -81,18 +82,13 @@ pub enum Event { BlinkChanged, } -#[derive(Clone, Debug)] -enum Scroll { - AlacScroll(AlacScroll), - ToNextSearch, -} - -#[derive(Clone, Debug)] +#[derive(Clone)] enum InternalEvent { - TermEvent(AlacTermEvent), + ColorRequest(usize, Arc String + Sync + Send + 'static>), Resize(TerminalSize), Clear, - Scroll(Scroll), + FocusNextMatch, + Scroll(AlacScroll), SetSelection(Option), UpdateSelection(Vector2F), Copy, @@ -172,8 +168,12 @@ impl From 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 { @@ -378,7 +378,6 @@ impl TerminalBuilder { cur_size: initial_size, last_mouse: None, last_offset: 0, - has_selection: false, searcher: None, }; @@ -451,7 +450,6 @@ pub struct Terminal { last_mode: TermMode, last_offset: usize, last_mouse: Option<(Point, Direction)>, - has_selection: bool, searcher: Option<(Option, Point)>, } @@ -492,9 +490,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 + } } } @@ -506,14 +506,12 @@ impl Terminal { cx: &mut ModelContext, ) { 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::().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::().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; @@ -526,17 +524,24 @@ impl Terminal { self.write_to_pty("\x0c".to_string()); term.clear_screen(ClearMode::Saved); } - InternalEvent::Scroll(Scroll::AlacScroll(scroll)) => { + InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); } - InternalEvent::Scroll(Scroll::ToNextSearch) => { + InternalEvent::FocusNextMatch => { if let Some((Some(searcher), origin)) = &self.searcher { match term.search_next(searcher, *origin, SEARCH_FORWARD, Direction::Left, None) { Some(regex_match) => { - //Jump to spot + term.scroll_to_point(*regex_match.start()); + + //Focus is done with selections in zed + let focus = make_selection(*regex_match.start(), *regex_match.end()); + term.selection = Some(focus); + } + None => { + //Clear focused match + term.selection = None; } - None => { /*reset state*/ } } } } @@ -560,7 +565,6 @@ impl Terminal { } fn begin_select(&mut self, sel: Selection) { - self.has_selection = true; self.events .push_back(InternalEvent::SetSelection(Some(sel))); } @@ -571,35 +575,30 @@ impl Terminal { } fn end_select(&mut self) { - self.has_selection = false; self.events.push_back(InternalEvent::SetSelection(None)); } fn scroll(&mut self, scroll: AlacScroll) { - self.events - .push_back(InternalEvent::Scroll(Scroll::AlacScroll(scroll))); + self.events.push_back(InternalEvent::Scroll(scroll)); } - fn scroll_to_next_search(&mut self) { - self.events - .push_back(InternalEvent::Scroll(Scroll::ToNextSearch)); + fn focus_next_match(&mut self) { + self.events.push_back(InternalEvent::FocusNextMatch); } pub fn search(&mut self, search: &str) { let new_searcher = RegexSearch::new(search).ok(); self.searcher = match (new_searcher, &self.searcher) { - //Existing search, carry over origin - (Some(new_searcher), Some((_, origin))) => Some((Some(new_searcher), *origin)), - //No existing search, start a new one - (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())), - //Error creating a new search, carry over origin - (None, Some((_, origin))) => Some((None, *origin)), //Nothing to do :( (None, None) => None, + //No existing search, start a new one + (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())), + //Existing search, carry over origin + (new_searcher, Some((_, origin))) => Some((new_searcher, *origin)), }; if let Some((Some(_), _)) = self.searcher { - self.scroll_to_next_search(); + self.focus_next_match(); } } @@ -658,7 +657,7 @@ impl Terminal { pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T where - F: FnOnce(RenderableContent, char) -> T, + F: FnOnce(RenderableContent, char, Option) -> T, { let m = self.term.clone(); //Arc clone let mut term = m.lock(); @@ -672,14 +671,15 @@ impl Terminal { let content = term.renderable_content(); - // term.line_search_right(point) - // term.search_next(dfas, origin, direction, side, max_lines) - self.last_offset = content.display_offset; let cursor_text = term.grid()[content.cursor.point].c; - f(content, cursor_text) + f( + content, + cursor_text, + self.searcher.as_ref().and_then(|s| s.0.clone()), + ) } pub fn focus_in(&self) { @@ -854,6 +854,12 @@ impl Terminal { } } +fn make_selection(from: Point, to: Point) -> Selection { + let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left); + focus.update(to, Direction::Right); + focus +} + impl Drop for Terminal { fn drop(&mut self) { self.pty_tx.0.send(Msg::Shutdown).ok(); diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 7cb3f032e4066ffdbfc9d8b7eb28dba941733323..2f6c0109273ab6661728f83100c3a6c53f5c32f9 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -43,7 +43,7 @@ use crate::{ pub struct LayoutState { cells: Vec, rects: Vec, - highlights: Vec, + selections: Vec, cursor: Option, background_color: Color, selection_color: Color, @@ -624,13 +624,13 @@ impl Element for TerminalElement { terminal_theme.colors.background }; - let (cells, selection, cursor, display_offset, cursor_text, mode) = self + let (cells, selection, cursor, display_offset, cursor_text, searcher, mode) = self .terminal .upgrade(cx) .unwrap() .update(cx.app, |terminal, mcx| { terminal.set_size(dimensions); - terminal.render_lock(mcx, |content, cursor_text| { + terminal.render_lock(mcx, |content, cursor_text, searcher| { let mut cells = vec![]; cells.extend( content @@ -653,12 +653,13 @@ impl Element for TerminalElement { content.cursor, content.display_offset, cursor_text, + searcher, content.mode, ) }) }); - let (cells, rects, highlights) = TerminalElement::layout_grid( + let (cells, rects, selections) = TerminalElement::layout_grid( cells, &text_style, &terminal_theme, @@ -731,7 +732,7 @@ impl Element for TerminalElement { selection_color, size: dimensions, rects, - highlights, + selections, mode, }, ) @@ -769,13 +770,13 @@ impl Element for TerminalElement { //Draw Selection cx.paint_layer(clip_bounds, |cx| { - let start_y = layout.highlights.get(0).map(|highlight| { + let start_y = layout.selections.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 + .selections .iter() .map(|relative_highlight| { relative_highlight.to_highlighted_range_line(origin, layout) From 63d9d29762baed65ac01a1d8e18ec4bdcad66bda Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 Aug 2022 16:17:17 -0700 Subject: [PATCH 3/8] Search rendering and basic regex stuff complete --- crates/terminal/src/search.rs | 78 ---------- crates/terminal/src/terminal.rs | 69 ++++++--- crates/terminal/src/terminal_element.rs | 193 +++++++++++++----------- 3 files changed, 155 insertions(+), 185 deletions(-) delete mode 100644 crates/terminal/src/search.rs diff --git a/crates/terminal/src/search.rs b/crates/terminal/src/search.rs deleted file mode 100644 index d9d7b076eb9ccb036cca67370222e159ba78f414..0000000000000000000000000000000000000000 --- a/crates/terminal/src/search.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::{borrow::Cow, ops::Deref}; - -use alacritty_terminal::{ - grid::Dimensions, - index::{Column, Direction, Line, Point}, - term::search::{Match, RegexIter, RegexSearch}, - Term, -}; - -const MAX_SEARCH_LINES: usize = 100; - -///Header and impl fom alacritty/src/display/content.rs HintMatches -#[derive(Default)] -pub struct SearchMatches<'a> { - /// All visible matches. - matches: Cow<'a, [Match]>, - - /// Index of the last match checked. - index: usize, -} - -impl<'a> SearchMatches<'a> { - /// Create new renderable matches iterator.. - fn new(matches: impl Into>) -> Self { - Self { - matches: matches.into(), - index: 0, - } - } - - /// Create from regex matches on term visable part. - pub fn visible_regex_matches(term: &Term, dfas: &RegexSearch) -> Self { - let matches = visible_regex_match_iter(term, dfas).collect::>(); - Self::new(matches) - } - - /// Advance the regex tracker to the next point. - /// - /// This will return `true` if the point passed is part of a regex match. - fn advance(&mut self, point: Point) -> bool { - while let Some(bounds) = self.get(self.index) { - if bounds.start() > &point { - break; - } else if bounds.end() < &point { - self.index += 1; - } else { - return true; - } - } - false - } -} - -impl<'a> Deref for SearchMatches<'a> { - type Target = [Match]; - - fn deref(&self) -> &Self::Target { - self.matches.deref() - } -} - -/// Copied from alacritty/src/display/hint.rs -/// Iterate over all visible regex matches. -fn visible_regex_match_iter<'a, T>( - term: &'a Term, - regex: &'a RegexSearch, -) -> impl Iterator + '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, Direction::Right, term, regex) - .skip_while(move |rm| rm.end().line < viewport_start) - .take_while(move |rm| rm.start().line <= viewport_end) -} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3b40452ee6fe3ddc6aa1b716a5708eafc04b5b97..0da7e1cf76e5f2834e88b25c2fc8926e722aeb3f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,6 +1,5 @@ pub mod mappings; pub mod modal; -pub mod search; pub mod terminal_container_view; pub mod terminal_element; pub mod terminal_view; @@ -11,10 +10,14 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll as AlacScroll}, - index::{Direction, Point}, + index::{Column, Direction, Line, Point}, selection::{Selection, SelectionType}, sync::FairMutex, - term::{color::Rgb, search::RegexSearch, RenderableContent, TermMode}, + term::{ + color::Rgb, + search::{Match, RegexIter, RegexSearch}, + RenderableContent, TermMode, + }, tty::{self, setup_env}, Term, }; @@ -28,6 +31,7 @@ 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}, @@ -66,7 +70,7 @@ pub fn init(cx: &mut MutableAppContext) { const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; // const ALACRITTY_SEARCH_LINE_LIMIT: usize = 1000; const SEARCH_FORWARD: Direction = Direction::Left; - +const MAX_SEARCH_LINES: usize = 100; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; @@ -528,9 +532,17 @@ impl Terminal { term.scroll_display(*scroll); } InternalEvent::FocusNextMatch => { - if let Some((Some(searcher), origin)) = &self.searcher { - match term.search_next(searcher, *origin, SEARCH_FORWARD, Direction::Left, None) - { + if let Some((Some(searcher), _origin)) = &self.searcher { + match term.search_next( + searcher, + Point { + line: Line(0), + column: Column(0), + }, + SEARCH_FORWARD, + Direction::Left, + None, + ) { Some(regex_match) => { term.scroll_to_point(*regex_match.start()); @@ -657,7 +669,7 @@ impl Terminal { pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T where - F: FnOnce(RenderableContent, char, Option) -> T, + F: FnOnce(RenderableContent, char, Vec) -> T, { let m = self.term.clone(); //Arc clone let mut term = m.lock(); @@ -675,11 +687,12 @@ impl Terminal { let cursor_text = term.grid()[content.cursor.point].c; - f( - content, - cursor_text, - self.searcher.as_ref().and_then(|s| s.0.clone()), - ) + let mut matches = vec![]; + if let Some((Some(r), _)) = &self.searcher { + matches.extend(make_search_matches(&term, &r)); + } + + f(content, cursor_text, matches) } pub fn focus_in(&self) { @@ -854,12 +867,6 @@ impl Terminal { } } -fn make_selection(from: Point, to: Point) -> Selection { - let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left); - focus.update(to, Direction::Right); - focus -} - impl Drop for Terminal { fn drop(&mut self) { self.pty_tx.0.send(Msg::Shutdown).ok(); @@ -870,6 +877,30 @@ impl Entity for Terminal { type Event = Event; } +fn make_selection(from: Point, to: Point) -> Selection { + let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left); + focus.update(to, Direction::Right); + focus +} + +/// 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, + regex: &'a RegexSearch, +) -> impl Iterator + '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, Direction::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; diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 2f6c0109273ab6661728f83100c3a6c53f5c32f9..9b3116052996e86f5a455bbf1d26c8cf741007e7 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -2,7 +2,6 @@ use alacritty_terminal::{ ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, grid::Dimensions, index::Point, - selection::SelectionRange, term::{ cell::{Cell, Flags}, TermMode, @@ -27,7 +26,7 @@ use settings::Settings; use theme::TerminalStyle; use util::ResultExt; -use std::fmt::Debug; +use std::{fmt::Debug, ops::RangeInclusive}; use std::{ mem, ops::{Deref, Range}, @@ -43,12 +42,12 @@ use crate::{ pub struct LayoutState { cells: Vec, rects: Vec, - selections: Vec, + relative_highlighted_ranges: Vec<(RangeInclusive, Color)>, cursor: Option, background_color: Color, - selection_color: Color, size: TerminalSize, mode: TermMode, + display_offset: usize, } #[derive(Debug)] @@ -166,30 +165,6 @@ impl LayoutRect { } } -#[derive(Clone, Debug, Default)] -struct RelativeHighlightedRange { - line_index: usize, - range: Range, -} - -impl RelativeHighlightedRange { - fn new(line_index: usize, range: Range) -> 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,6 +192,8 @@ impl TerminalElement { } } + //Vec> -> Clip out the parts of the ranges + fn layout_grid( grid: Vec, text_style: &TextStyle, @@ -224,41 +201,22 @@ impl TerminalElement { text_layout_cache: &TextLayoutCache, font_cache: &FontCache, modal: bool, - selection_range: Option, - ) -> ( - Vec, - Vec, - Vec, - ) { + ) -> (Vec, Vec) { let mut cells = vec![]; let mut rects = vec![]; - let mut highlight_ranges = vec![]; let mut cur_rect: Option = 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 +282,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,6 +563,7 @@ 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); @@ -624,13 +576,13 @@ impl Element for TerminalElement { terminal_theme.colors.background }; - let (cells, selection, cursor, display_offset, cursor_text, searcher, mode) = self + let (cells, selection, cursor, display_offset, cursor_text, search_matches, mode) = self .terminal .upgrade(cx) .unwrap() - .update(cx.app, |terminal, mcx| { + .update(cx.app, |terminal, cx| { terminal.set_size(dimensions); - terminal.render_lock(mcx, |content, cursor_text, searcher| { + terminal.render_lock(cx, |content, cursor_text, search_matches| { let mut cells = vec![]; cells.extend( content @@ -653,20 +605,30 @@ impl Element for TerminalElement { content.cursor, content.display_offset, cursor_text, - searcher, + search_matches.clone(), content.mode, ) }) }); - let (cells, rects, selections) = TerminalElement::layout_grid( + // searches, highlights to a single range representations + let mut relative_highlighted_ranges = Vec::new(); + if let Some(selection) = selection { + relative_highlighted_ranges.push((selection.start..=selection.end, selection_color)); + } + for search_match in search_matches { + relative_highlighted_ranges.push((search_match, match_color)) + } + + // then have that representation be converted to the appropriate highlight data structure + + 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 @@ -729,11 +691,11 @@ impl Element for TerminalElement { cells, cursor, background_color, - selection_color, size: dimensions, rects, - selections, + relative_highlighted_ranges, mode, + display_offset, }, ) } @@ -768,30 +730,23 @@ impl Element for TerminalElement { } }); - //Draw Selection + //Draw Highlighted Backgrounds cx.paint_layer(clip_bounds, |cx| { - let start_y = layout.selections.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 - .selections - .iter() - .map(|relative_highlight| { - relative_highlight.to_highlighted_range_line(origin, layout) - }) - .collect::>(); - - 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); + } } }); @@ -894,3 +849,65 @@ impl Element for TerminalElement { Some(layout.cursor.as_ref()?.bounding_rect(origin)) } } + +fn to_highlighted_range_lines( + range: &RangeInclusive, + layout: &LayoutState, + origin: Vector2F, +) -> Option<(f32, Vec)> { + // 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)) +} From 3f11fd3b8bffe30b14e8f7317d032b4b23678848 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 Aug 2022 17:41:53 -0700 Subject: [PATCH 4/8] Terminal implements important half of search protocol --- assets/keymaps/default.json | 3 +- crates/editor/src/items.rs | 14 +- crates/search/src/buffer_search.rs | 10 +- crates/terminal/src/terminal.rs | 167 ++++++++++-------- .../terminal/src/terminal_container_view.rs | 99 ++++++++++- crates/terminal/src/terminal_element.rs | 17 +- crates/terminal/src/terminal_view.rs | 31 ++-- crates/workspace/src/searchable.rs | 29 +-- 8 files changed, 250 insertions(+), 120 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2fb1f010a66fc453b425ddfdba0ea5de566305c1..1d95c33cbf1edc3fc4ee4a8bd1c4a7f8a3118418 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -424,8 +424,7 @@ "ctrl-cmd-space": "terminal::ShowCharacterPalette", "cmd-c": "terminal::Copy", "cmd-v": "terminal::Paste", - "cmd-k": "terminal::Clear", - "cmd-s": "terminal::SearchTest" + "cmd-k": "terminal::Clear" } }, { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b147d2af2cd5229fb7748bffe4e3569539e56d14..e731eb98b5d37c7ed2f95f2a1c83fc3eecdaa6d8 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -513,17 +513,17 @@ impl SearchableItem for Editor { fn to_search_event(event: &Self::Event) -> Option { 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) { + fn clear_matches(&mut self, cx: &mut ViewContext) { self.clear_background_highlights::(cx); } - fn highlight_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { + fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { self.highlight_background::( matches, |theme| theme.search.match_background, @@ -553,7 +553,7 @@ impl SearchableItem for Editor { } } - fn select_next_match_in_direction( + fn activate_next_match( &mut self, index: usize, direction: Direction, @@ -575,7 +575,7 @@ impl SearchableItem for Editor { }); } - fn select_match_by_index( + fn activate_match_at_index( &mut self, index: usize, matches: Vec>, @@ -586,7 +586,7 @@ impl SearchableItem for Editor { }); } - fn matches( + fn find_matches( &mut self, query: project::search::SearchQuery, cx: &mut ViewContext, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a2fb342e3f2e011921fc11a1071450f58b28b671..1a46526025f50516ef1fdedcb86c681c249a8785 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -174,7 +174,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) + }); } }), )); @@ -461,10 +463,10 @@ impl BufferSearchBar { } } - fn on_active_editor_event(&mut self, event: SearchEvent, cx: &mut ViewContext) { + fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext) { 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), } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 0da7e1cf76e5f2834e88b25c2fc8926e722aeb3f..4adaa2140f02e954e02df6912da82084e926291e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -36,7 +36,7 @@ use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; use std::{ collections::{HashMap, VecDeque}, fmt::Display, - ops::Sub, + ops::{RangeInclusive, Sub}, path::PathBuf, sync::Arc, time::Duration, @@ -48,7 +48,7 @@ use gpui::{ keymap::Keystroke, scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent}, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, - ScrollWheelEvent, + ScrollWheelEvent, Task, }; use crate::mappings::{ @@ -68,8 +68,6 @@ pub fn init(cx: &mut MutableAppContext) { ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; -// const ALACRITTY_SEARCH_LINE_LIMIT: usize = 1000; -const SEARCH_FORWARD: Direction = Direction::Left; const MAX_SEARCH_LINES: usize = 100; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; @@ -91,7 +89,7 @@ enum InternalEvent { ColorRequest(usize, Arc String + Sync + Send + 'static>), Resize(TerminalSize), Clear, - FocusNextMatch, + // FocusNextMatch, Scroll(AlacScroll), SetSelection(Option), UpdateSelection(Vector2F), @@ -382,7 +380,8 @@ impl TerminalBuilder { cur_size: initial_size, last_mouse: None, last_offset: 0, - searcher: None, + matches: Vec::new(), + selection_text: None, }; Ok(TerminalBuilder { @@ -454,7 +453,8 @@ pub struct Terminal { last_mode: TermMode, last_offset: usize, last_mouse: Option<(Point, Direction)>, - searcher: Option<(Option, Point)>, + pub matches: Vec>, + pub selection_text: Option, } impl Terminal { @@ -531,32 +531,32 @@ impl Terminal { InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); } - InternalEvent::FocusNextMatch => { - if let Some((Some(searcher), _origin)) = &self.searcher { - match term.search_next( - searcher, - Point { - line: Line(0), - column: Column(0), - }, - SEARCH_FORWARD, - Direction::Left, - None, - ) { - Some(regex_match) => { - term.scroll_to_point(*regex_match.start()); - - //Focus is done with selections in zed - let focus = make_selection(*regex_match.start(), *regex_match.end()); - term.selection = Some(focus); - } - None => { - //Clear focused match - term.selection = None; - } - } - } - } + // InternalEvent::FocusNextMatch => { + // if let Some((Some(searcher), _origin)) = &self.searcher { + // match term.search_next( + // searcher, + // Point { + // line: Line(0), + // column: Column(0), + // }, + // SEARCH_FORWARD, + // Direction::Left, + // None, + // ) { + // Some(regex_match) => { + // term.scroll_to_point(*regex_match.start()); + + // //Focus is done with selections in zed + // let focus = make_selection(*regex_match.start(), *regex_match.end()); + // term.selection = Some(focus); + // } + // None => { + // //Clear focused match + // term.selection = None; + // } + // } + // } + // } InternalEvent::SetSelection(sel) => term.selection = sel.clone(), InternalEvent::UpdateSelection(position) => { if let Some(mut selection) = term.selection.take() { @@ -594,34 +594,34 @@ impl Terminal { self.events.push_back(InternalEvent::Scroll(scroll)); } - fn focus_next_match(&mut self) { - self.events.push_back(InternalEvent::FocusNextMatch); - } + // fn focus_next_match(&mut self) { + // self.events.push_back(InternalEvent::FocusNextMatch); + // } - pub fn search(&mut self, search: &str) { - let new_searcher = RegexSearch::new(search).ok(); - self.searcher = match (new_searcher, &self.searcher) { - //Nothing to do :( - (None, None) => None, - //No existing search, start a new one - (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())), - //Existing search, carry over origin - (new_searcher, Some((_, origin))) => Some((new_searcher, *origin)), - }; + // pub fn search(&mut self, search: &str) { + // let new_searcher = RegexSearch::new(search).ok(); + // self.searcher = match (new_searcher, &self.searcher) { + // //Nothing to do :( + // (None, None) => None, + // //No existing search, start a new one + // (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())), + // //Existing search, carry over origin + // (new_searcher, Some((_, origin))) => Some((new_searcher, *origin)), + // }; - if let Some((Some(_), _)) = self.searcher { - self.focus_next_match(); - } - } + // if let Some((Some(_), _)) = self.searcher { + // self.focus_next_match(); + // } + // } - fn viewport_origin(&mut self) -> Point { - let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1; - Point::new(viewport_top, alacritty_terminal::index::Column(0)) - } + // fn viewport_origin(&mut self) -> Point { + // let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1; + // Point::new(viewport_top, alacritty_terminal::index::Column(0)) + // } - pub fn end_search(&mut self) { - self.searcher = None; - } + // pub fn end_search(&mut self) { + // self.searcher = None; + // } pub fn copy(&mut self) { self.events.push_back(InternalEvent::Copy); @@ -669,12 +669,12 @@ impl Terminal { pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T where - F: FnOnce(RenderableContent, char, Vec) -> T, + F: FnOnce(RenderableContent, char) -> T, { - let m = self.term.clone(); //Arc clone - let mut term = m.lock(); + let term = self.term.clone(); + let mut term = term.lock(); - //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) } @@ -683,16 +683,12 @@ impl Terminal { let content = term.renderable_content(); + self.selection_text = term.selection_to_string(); self.last_offset = content.display_offset; let cursor_text = term.grid()[content.cursor.point].c; - let mut matches = vec![]; - if let Some((Some(r), _)) = &self.searcher { - matches.extend(make_search_matches(&term, &r)); - } - - f(content, cursor_text, matches) + f(content, cursor_text) } pub fn focus_in(&self) { @@ -865,6 +861,33 @@ impl Terminal { } } } + + pub fn find_matches( + &mut self, + query: project::search::SearchQuery, + cx: &mut ModelContext, + ) -> Task>> { + let term = self.term.clone(); + dbg!("Spawning find_matches"); + 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(); + dbg!(make_search_matches(&term, &searcher).collect()) + }) + } } impl Drop for Terminal { @@ -877,11 +900,11 @@ impl Entity for Terminal { type Event = Event; } -fn make_selection(from: Point, to: Point) -> Selection { - let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left); - focus.update(to, Direction::Right); - focus -} +// fn make_selection(from: Point, to: Point) -> Selection { +// let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left); +// focus.update(to, Direction::Right); +// focus +// } /// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches() /// Iterate over all visible regex matches. diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 02c018c82b2b307a8eee91babe3b358f6ef9c1f5..d93dfe4a78d49d3770ada9ad2405748201da3393 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/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::{Direction, SearchEvent, 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; @@ -328,6 +331,98 @@ impl Item for TerminalContainer { fn should_close_item_on_event(event: &Self::Event) -> bool { matches!(event, &Event::CloseTerminal) } + + fn as_searchable(&self, handle: &ViewHandle) -> Option> { + Some(Box::new(handle.clone())) + } +} + +impl SearchableItem for TerminalContainer { + type Match = RangeInclusive; + + /// Convert events raised by this item into search-relevant events (if applicable) + fn to_search_event(event: &Self::Event) -> Option { + match event { + Event::Wakeup => Some(SearchEvent::MatchesInvalidated), + //TODO selection changed + _ => None, + } + } + + /// Clear stored matches + fn clear_matches(&mut self, cx: &mut ViewContext) { + if let TerminalContent::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, cx: &mut ViewContext) { + if let TerminalContent::Connected(connected) = &self.content { + let terminal = connected.read(cx).terminal().clone(); + dbg!(&matches); + 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) -> String { + if let TerminalContent::Connected(connected) = &self.content { + let terminal = connected.read(cx).terminal().clone(); + terminal.read(cx).selection_text.clone().unwrap_or_default() + } else { + Default::default() + } + } + + /// Given an index, a set of matches for this index, and a direction, + /// get the next match (clicking the arrow) + fn activate_next_match( + &mut self, + _index: usize, + _direction: Direction, + _matches: Vec, + _cx: &mut ViewContext, + ) { + // TODO: + } + + /// Focus match at given index into the Vec of matches + fn activate_match_at_index( + &mut self, + _index: usize, + _matches: Vec, + _cx: &mut ViewContext, + ) { + } + + /// 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, + ) -> Task> { + if let TerminalContent::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, + _cx: &mut ViewContext, + ) -> Option { + if matches.len() > 0 { + Some(0) + } else { + None + } + } } ///Get's the working directory for the given workspace, respecting the user's settings. diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 9b3116052996e86f5a455bbf1d26c8cf741007e7..fd6c3883d242ec1556f332abf6b25b5b68132843 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -570,19 +570,25 @@ impl Element for TerminalElement { 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 (cells, selection, cursor, display_offset, cursor_text, search_matches, mode) = self + let (cells, selection, cursor, display_offset, cursor_text, mode) = self .terminal .upgrade(cx) .unwrap() .update(cx.app, |terminal, cx| { terminal.set_size(dimensions); - terminal.render_lock(cx, |content, cursor_text, search_matches| { + terminal.render_lock(cx, |content, cursor_text| { let mut cells = vec![]; cells.extend( content @@ -605,7 +611,6 @@ impl Element for TerminalElement { content.cursor, content.display_offset, cursor_text, - search_matches.clone(), content.mode, ) }) @@ -613,12 +618,12 @@ impl Element for TerminalElement { // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); - if let Some(selection) = selection { - relative_highlighted_ranges.push((selection.start..=selection.end, selection_color)); - } 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 diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 0e067ac0fb93dfaf214028621e14baa764982f49..fee84dc8597e07f3b12e0c4ed2c85ab39351c221 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/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; @@ -58,8 +58,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(TerminalView::paste); cx.add_action(TerminalView::clear); cx.add_action(TerminalView::show_character_palette); - - cx.add_action(TerminalView::test_search); } ///A terminal view, maintains the PTY's file handles and communicates with the terminal @@ -162,14 +160,6 @@ impl TerminalView { } } - fn test_search(&mut self, _: &SearchTest, cx: &mut ViewContext) { - let search_string = "ttys"; - self.terminal.update(cx, |term, _| { - term.search(search_string); - }); - cx.notify(); - } - fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { self.terminal.update(cx, |term, _| term.clear()); cx.notify(); @@ -246,6 +236,19 @@ impl TerminalView { .detach(); } + pub fn find_matches( + &mut self, + query: project::search::SearchQuery, + cx: &mut ViewContext, + ) -> Task>> { + self.terminal + .update(cx, |term, cx| term.find_matches(query, cx)) + } + + pub fn terminal(&self) -> &ModelHandle { + &self.terminal + } + fn next_blink_epoch(&mut self) -> usize { self.blink_epoch += 1; self.blink_epoch diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index b0791ff4b3d03d89d0e9718e75c2631a93b15700..de0d4f774a832e1c393be3691d23fcbfe9f0ccce 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -10,8 +10,8 @@ use crate::{Item, ItemHandle, WeakItemHandle}; #[derive(Debug)] pub enum SearchEvent { - ContentsUpdated, - SelectionsChanged, + MatchesInvalidated, + ActiveMatchChanged, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -24,24 +24,27 @@ pub trait SearchableItem: Item { type Match: Any + Sync + Send + Clone; fn to_search_event(event: &Self::Event) -> Option; - fn clear_highlights(&mut self, cx: &mut ViewContext); - fn highlight_matches(&mut self, matches: Vec, cx: &mut ViewContext); + fn clear_matches(&mut self, cx: &mut ViewContext); + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; - fn select_next_match_in_direction( + fn activate_next_match( &mut self, index: usize, direction: Direction, matches: Vec, cx: &mut ViewContext, ); - fn select_match_by_index( + fn activate_match_at_index( &mut self, index: usize, matches: Vec, cx: &mut ViewContext, ); - fn matches(&mut self, query: SearchQuery, cx: &mut ViewContext) - -> Task>; + fn find_matches( + &mut self, + query: SearchQuery, + cx: &mut ViewContext, + ) -> Task>; fn active_match_index( &mut self, matches: Vec, @@ -107,11 +110,11 @@ impl SearchableItemHandle for ViewHandle { } fn clear_highlights(&self, cx: &mut MutableAppContext) { - self.update(cx, |this, cx| this.clear_highlights(cx)); + self.update(cx, |this, cx| this.clear_matches(cx)); } fn highlight_matches(&self, matches: &Vec>, 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)) @@ -125,7 +128,7 @@ impl SearchableItemHandle for ViewHandle { ) { let matches = downcast_matches(matches); self.update(cx, |this, cx| { - this.select_next_match_in_direction(index, direction, matches, cx) + this.activate_next_match(index, direction, matches, cx) }); } fn select_match_by_index( @@ -136,7 +139,7 @@ impl SearchableItemHandle for ViewHandle { ) { let matches = downcast_matches(matches); self.update(cx, |this, cx| { - this.select_match_by_index(index, matches, cx) + this.activate_match_at_index(index, matches, cx) }); } fn matches( @@ -144,7 +147,7 @@ impl SearchableItemHandle for ViewHandle { query: SearchQuery, cx: &mut MutableAppContext, ) -> Task>> { - 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 From a8b800398063938b75927850987ebf0546767aca Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 31 Aug 2022 18:27:25 -0700 Subject: [PATCH 5/8] ?? --- crates/terminal/src/terminal_element.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index fd6c3883d242ec1556f332abf6b25b5b68132843..828ae20ab68e099896353a391510773cd039ce91 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -618,12 +618,12 @@ impl Element for TerminalElement { // 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)); } + for search_match in search_matches { + relative_highlighted_ranges.push((search_match, match_color)) + } // then have that representation be converted to the appropriate highlight data structure From faad24542fa262d07e713b083ec334eabd85b10c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Sep 2022 11:43:27 -0700 Subject: [PATCH 6/8] Improved performance of terminal rendering further --- crates/terminal/src/modal.rs | 7 +- crates/terminal/src/terminal.rs | 182 +++++++++++++----- .../terminal/src/terminal_container_view.rs | 40 ++-- crates/terminal/src/terminal_element.rs | 85 +++----- crates/terminal/src/terminal_view.rs | 11 +- styles/package-lock.json | 1 - 6 files changed, 194 insertions(+), 132 deletions(-) diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 1aea96277a56f561ab9545988f836d4d3e4fe5a2..504a4b84ab12c8665afdb0b2fa95db199f5d4998 100644 --- a/crates/terminal/src/modal.rs +++ b/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, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 4adaa2140f02e954e02df6912da82084e926291e..c375babf8487c4fbbf4600ca50cd17b615385531 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -11,17 +11,19 @@ use alacritty_terminal::{ event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll as AlacScroll}, index::{Column, Direction, Line, Point}, - selection::{Selection, SelectionType}, + selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ + cell::Cell, color::Rgb, search::{Match, RegexIter, RegexSearch}, - RenderableContent, TermMode, + RenderableCursor, TermMode, }, tty::{self, setup_env}, Term, }; use anyhow::{bail, Result}; + use futures::{ channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, FutureExt, @@ -36,10 +38,10 @@ use settings::{AlternateScroll, Settings, Shell, TerminalBlink}; use std::{ collections::{HashMap, VecDeque}, fmt::Display, - ops::{RangeInclusive, Sub}, + ops::{Deref, RangeInclusive, Sub}, path::PathBuf, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use thiserror::Error; @@ -376,12 +378,12 @@ 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, matches: Vec::new(), - selection_text: None, + last_synced: Instant::now(), + sync_task: None, }; Ok(TerminalBuilder { @@ -443,18 +445,61 @@ 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, + mode: TermMode, + display_offset: usize, + selection_text: Option, + selection: Option, + 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>>, events: VecDeque, default_title: String, title: String, - cur_size: TerminalSize, - last_mode: TermMode, - last_offset: usize, last_mouse: Option<(Point, Direction)>, pub matches: Vec>, - pub selection_text: Option, + cur_size: TerminalSize, + last_content: TerminalContent, + last_synced: Instant, + sync_task: Option>, } impl Terminal { @@ -576,6 +621,10 @@ impl Terminal { } } + pub fn last_content(&self) -> &TerminalContent { + &self.last_content + } + fn begin_select(&mut self, sel: Selection) { self.events .push_back(InternalEvent::SetSelection(Some(sel))); @@ -648,7 +697,7 @@ impl Terminal { } 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 @@ -659,7 +708,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") @@ -667,38 +716,76 @@ impl Terminal { self.input(paste_text) } - pub fn render_lock(&mut self, cx: &mut ModelContext, f: F) -> T - where - F: FnOnce(RenderableContent, char) -> T, - { + pub fn try_sync(&mut self, cx: &mut ModelContext) { let term = self.term.clone(); - let mut term = term.lock(); + + 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 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) -> TerminalContent { let content = term.renderable_content(); - - self.selection_text = term.selection_to_string(); - 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::>(), + 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()); } } @@ -721,17 +808,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); } } @@ -746,7 +833,7 @@ impl Terminal { self.continue_selection(position); // Doesn't make sense to scroll the alt screen - if !self.last_mode.contains(TermMode::ALT_SCREEN) { + if !self.last_content.mode.contains(TermMode::ALT_SCREEN) { let scroll_delta = match self.drag_line_delta(e) { Some(value) => value, None => return, @@ -775,11 +862,11 @@ impl Terminal { 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 { @@ -791,7 +878,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 { @@ -814,9 +901,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 { @@ -835,15 +922,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 { @@ -868,7 +962,6 @@ impl Terminal { cx: &mut ModelContext, ) -> Task>> { let term = self.term.clone(); - dbg!("Spawning find_matches"); cx.background().spawn(async move { let searcher = match query { project::search::SearchQuery::Text { query, .. } => { @@ -885,7 +978,8 @@ impl Terminal { let searcher = searcher.unwrap(); let term = term.lock(); - dbg!(make_search_matches(&term, &searcher).collect()) + + make_search_matches(&term, &searcher).collect() }) } } diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index d93dfe4a78d49d3770ada9ad2405748201da3393..204ff241129e89e52c90d7f8b2c10109554f2522 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -29,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), Error(ViewHandle), } -impl TerminalContent { +impl TerminalContainerContent { fn handle(&self) -> AnyViewHandle { match self { Self::Connected(handle) => handle.into(), @@ -45,7 +45,7 @@ impl TerminalContent { pub struct TerminalContainer { modal: bool, - pub content: TerminalContent, + pub content: TerminalContainerContent, associated_directory: Option, } @@ -119,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::().unwrap(), }); - TerminalContent::Error(view) + TerminalContainerContent::Error(view) } }; cx.focus(content.handle()); @@ -145,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, } } @@ -158,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::(); @@ -238,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() @@ -309,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 @@ -317,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 @@ -351,7 +351,7 @@ impl SearchableItem for TerminalContainer { /// Clear stored matches fn clear_matches(&mut self, cx: &mut ViewContext) { - if let TerminalContent::Connected(connected) = &self.content { + if let TerminalContainerContent::Connected(connected) = &self.content { let terminal = connected.read(cx).terminal().clone(); terminal.update(cx, |term, _| term.matches.clear()) } @@ -359,18 +359,22 @@ impl SearchableItem for TerminalContainer { /// Store matches returned from find_matches somewhere for rendering fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - if let TerminalContent::Connected(connected) = &self.content { + if let TerminalContainerContent::Connected(connected) = &self.content { let terminal = connected.read(cx).terminal().clone(); - dbg!(&matches); 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) -> String { - if let TerminalContent::Connected(connected) = &self.content { + if let TerminalContainerContent::Connected(connected) = &self.content { let terminal = connected.read(cx).terminal().clone(); - terminal.read(cx).selection_text.clone().unwrap_or_default() + terminal + .read(cx) + .last_content + .selection_text + .clone() + .unwrap_or_default() } else { Default::default() } @@ -403,7 +407,7 @@ impl SearchableItem for TerminalContainer { query: project::search::SearchQuery, cx: &mut ViewContext, ) -> Task> { - if let TerminalContent::Connected(connected) = &self.content { + 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 { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 828ae20ab68e099896353a391510773cd039ce91..2f3d06e2b76ab797d6717555fdb6271aea87a324 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -2,10 +2,7 @@ use alacritty_terminal::{ ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, grid::Dimensions, index::Point, - term::{ - cell::{Cell, Flags}, - TermMode, - }, + term::{cell::Flags, TermMode}, }; use editor::{Cursor, CursorShape, HighlightedRange, HighlightedRangeLine}; use gpui::{ @@ -27,15 +24,12 @@ use theme::TerminalStyle; use util::ResultExt; use std::{fmt::Debug, ops::RangeInclusive}; -use std::{ - mem, - ops::{Deref, Range}, -}; +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 @@ -50,21 +44,6 @@ pub struct LayoutState { display_offset: usize, } -#[derive(Debug)] -struct IndexedCell { - point: Point, - cell: Cell, -} - -impl Deref for IndexedCell { - type Target = Cell; - - #[inline] - fn deref(&self) -> &Cell { - &self.cell - } -} - ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points struct DisplayCursor { line: i32, @@ -195,7 +174,7 @@ impl TerminalElement { //Vec> -> Clip out the parts of the ranges fn layout_grid( - grid: Vec, + grid: &Vec, text_style: &TextStyle, terminal_theme: &TerminalStyle, text_layout_cache: &TextLayoutCache, @@ -581,40 +560,22 @@ impl Element for TerminalElement { } 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, cx| { - terminal.set_size(dimensions); - terminal.render_lock(cx, |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(); @@ -641,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 @@ -699,8 +660,8 @@ impl Element for TerminalElement { size: dimensions, rects, relative_highlighted_ranges, - mode, - display_offset, + mode: *mode, + display_offset: *display_offset, }, ) } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index fee84dc8597e07f3b12e0c4ed2c85ab39351c221..1c49f8b3c2b30054f1cb90c732dfcf840f92931b 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -149,7 +149,8 @@ impl TerminalView { if !self .terminal .read(cx) - .last_mode + .last_content + .mode .contains(TermMode::ALT_SCREEN) { cx.show_character_palette(); @@ -177,7 +178,8 @@ impl TerminalView { || self .terminal .read(cx) - .last_mode + .last_content + .mode .contains(TermMode::ALT_SCREEN) { return true; @@ -362,7 +364,8 @@ impl View for TerminalView { if self .terminal .read(cx) - .last_mode + .last_content + .mode .contains(TermMode::ALT_SCREEN) { None @@ -387,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) { diff --git a/styles/package-lock.json b/styles/package-lock.json index 582f1c84968a5c1a25ddac5fd3c21ba907353c6d..5499f1852cb4330467268dee6436b53589a90e9b 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": { From 25aae1107b436ea0a2316118be8620a0cf1c9d62 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Sep 2022 11:55:15 -0700 Subject: [PATCH 7/8] Added cursor I-Beam --- crates/terminal/src/terminal_element.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index 2f3d06e2b76ab797d6717555fdb6271aea87a324..c5dc80d62b88d883b5d81ad1249c105436dbf29c 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -682,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 { From ebae991cb2e65c328c622c8ec3d06ff3dd17b5f9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Sep 2022 13:45:46 -0700 Subject: [PATCH 8/8] Finished terminal search --- crates/editor/src/items.rs | 91 +++++------ crates/language/src/syntax_map.rs | 1 - crates/search/src/buffer_search.rs | 100 +++++++----- crates/search/src/project_search.rs | 22 ++- crates/terminal/src/terminal.rs | 143 +++++++----------- .../terminal/src/terminal_container_view.rs | 59 +++++--- crates/terminal/src/terminal_element.rs | 6 +- crates/workspace/src/searchable.rs | 93 ++++++++---- 8 files changed, 270 insertions(+), 245 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index e731eb98b5d37c7ed2f95f2a1c83fc3eecdaa6d8..3d412c423ec9ec38ddda520026cf75cc2c1b5c57 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -553,37 +553,53 @@ impl SearchableItem for Editor { } } - fn activate_next_match( + fn activate_match( &mut self, index: usize, - direction: Direction, matches: Vec>, cx: &mut ViewContext, ) { - 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 activate_match_at_index( + fn match_index_for_direction( &mut self, - index: usize, - matches: Vec>, + matches: &Vec>, + mut current_index: usize, + direction: Direction, cx: &mut ViewContext, - ) { - 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 find_matches( @@ -637,41 +653,6 @@ impl SearchableItem for Editor { } } -pub fn match_index_for_direction( - ranges: &[Range], - 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], cursor: &Anchor, diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index d1bf698e52a07c26c38cffc4576f287e416bb0fd..a8cac76ac7a05664c3cbb5f3acabb0f5b416b5ce 100644 --- a/crates/language/src/syntax_map.rs +++ b/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 { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 1a46526025f50516ef1fdedcb86c681c249a8785..22574b9b718f6cec0619aaaf14faabcb8db749f0 100644 --- a/crates/search/src/buffer_search.rs +++ b/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() @@ -234,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() { @@ -283,36 +304,43 @@ impl BufferSearchBar { fn render_search_option( &self, + option_supported: bool, icon: &str, option: SearchOption, cx: &mut RenderContext, - ) -> ElementBox { + ) -> Option { + if !option_supported { + return None; + } + let tooltip_style = cx.global::().theme.tooltip.clone(); let is_active = self.is_search_option_enabled(option); - MouseEventHandler::new::(option as usize, cx, |state, cx| { - let style = &cx - .global::() - .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::( - option as usize, - format!("Toggle {}", option.label()), - Some(option.to_toggle_action()), - tooltip_style, - cx, + Some( + MouseEventHandler::new::(option as usize, cx, |state, cx| { + let style = &cx + .global::() + .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::( + option as usize, + format!("Toggle {}", option.label()), + Some(option.to_toggle_action()), + tooltip_style, + cx, + ) + .boxed(), ) - .boxed() } fn render_nav_button( @@ -422,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); } } } @@ -479,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); } } } @@ -494,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) { @@ -509,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 { @@ -529,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(); } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8c5723c30b83cdc836834ec2a3191e48ba81fe6e..8caa7bf71de3a36b67c51b7e761e653236fb1ff2 100644 --- a/crates/search/src/project_search.rs +++ b/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) { 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| { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index c375babf8487c4fbbf4600ca50cd17b615385531..f542477b4c34ae11901799dcbfe2f9435d45c0cf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -10,7 +10,7 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll as AlacScroll}, - index::{Column, Direction, Line, Point}, + index::{Column, Direction as AlacDirection, Line, Point}, selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ @@ -84,6 +84,7 @@ pub enum Event { Bell, Wakeup, BlinkChanged, + SelectionsChanged, } #[derive(Clone)] @@ -93,7 +94,8 @@ enum InternalEvent { Clear, // FocusNextMatch, Scroll(AlacScroll), - SetSelection(Option), + ScrollToPoint(Point), + SetSelection(Option<(Selection, Point)>), UpdateSelection(Vector2F), Copy, } @@ -384,6 +386,7 @@ impl TerminalBuilder { matches: Vec::new(), last_synced: Instant::now(), sync_task: None, + selection_head: None, }; Ok(TerminalBuilder { @@ -494,12 +497,13 @@ pub struct Terminal { events: VecDeque, default_title: String, title: String, - last_mouse: Option<(Point, Direction)>, + last_mouse: Option<(Point, AlacDirection)>, pub matches: Vec>, cur_size: TerminalSize, last_content: TerminalContent, last_synced: Instant, sync_task: Option>, + selection_head: Option, } impl Terminal { @@ -576,33 +580,14 @@ impl Terminal { InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); } - // InternalEvent::FocusNextMatch => { - // if let Some((Some(searcher), _origin)) = &self.searcher { - // match term.search_next( - // searcher, - // Point { - // line: Line(0), - // column: Column(0), - // }, - // SEARCH_FORWARD, - // Direction::Left, - // None, - // ) { - // Some(regex_match) => { - // term.scroll_to_point(*regex_match.start()); - - // //Focus is done with selections in zed - // let focus = make_selection(*regex_match.start(), *regex_match.end()); - // term.selection = Some(focus); - // } - // None => { - // //Clear focused match - // term.selection = None; - // } - // } - // } - // } - 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()); @@ -610,6 +595,9 @@ impl Terminal { selection.update(point, side); term.selection = Some(selection); + + self.selection_head = Some(point); + cx.emit(Event::SelectionsChanged) } } @@ -618,6 +606,7 @@ impl Terminal { cx.write_to_clipboard(ClipboardItem::new(txt)) } } + InternalEvent::ScrollToPoint(point) => term.scroll_to_point(*point), } } @@ -625,53 +614,24 @@ impl Terminal { &self.last_content } - fn begin_select(&mut self, sel: Selection) { - self.events - .push_back(InternalEvent::SetSelection(Some(sel))); - } + //To test: + //- Activate match on terminal (scrolling and selection) + //- Editor search snapping behavior - fn continue_selection(&mut self, location: Vector2F) { - self.events - .push_back(InternalEvent::UpdateSelection(location)) - } + 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()))); - fn end_select(&mut self) { - self.events.push_back(InternalEvent::SetSelection(None)); + self.events + .push_back(InternalEvent::ScrollToPoint(*search_match.start())); + } } - fn scroll(&mut self, scroll: AlacScroll) { - self.events.push_back(InternalEvent::Scroll(scroll)); + fn set_selection(&mut self, selection: Option<(Selection, Point)>) { + self.events + .push_back(InternalEvent::SetSelection(selection)); } - // fn focus_next_match(&mut self) { - // self.events.push_back(InternalEvent::FocusNextMatch); - // } - - // pub fn search(&mut self, search: &str) { - // let new_searcher = RegexSearch::new(search).ok(); - // self.searcher = match (new_searcher, &self.searcher) { - // //Nothing to do :( - // (None, None) => None, - // //No existing search, start a new one - // (Some(new_searcher), None) => Some((Some(new_searcher), self.viewport_origin())), - // //Existing search, carry over origin - // (new_searcher, Some((_, origin))) => Some((new_searcher, *origin)), - // }; - - // if let Some((Some(_), _)) = self.searcher { - // self.focus_next_match(); - // } - // } - - // fn viewport_origin(&mut self) -> Point { - // let viewport_top = alacritty_terminal::index::Line(-(self.last_offset as i32)) - 1; - // Point::new(viewport_top, alacritty_terminal::index::Column(0)) - // } - - // pub fn end_search(&mut self) { - // self.searcher = None; - // } - pub fn copy(&mut self) { self.events.push_back(InternalEvent::Copy); } @@ -691,8 +651,10 @@ impl Terminal { } pub fn input(&mut self, input: String) { - self.scroll(AlacScroll::Bottom); - self.end_select(); + self.events + .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::SetSelection(None)); + self.write_to_pty(input); } @@ -790,7 +752,7 @@ impl Terminal { } } - 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 { @@ -830,7 +792,8 @@ 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_content.mode.contains(TermMode::ALT_SCREEN) { @@ -840,8 +803,11 @@ impl Terminal { }; let scroll_lines = (scroll_delta / self.cur_size.line_height) as i32; - self.scroll(AlacScroll::Delta(scroll_lines)); - self.continue_selection(position) + + self.events + .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); + self.events + .push_back(InternalEvent::UpdateSelection(position)) } } } @@ -870,7 +836,10 @@ impl Terminal { 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, + )))); } } @@ -893,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)))); } } } @@ -951,7 +921,8 @@ impl Terminal { ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32; if scroll_lines != 0 { let scroll = AlacScroll::Delta(scroll_lines); - self.scroll(scroll); + + self.events.push_back(InternalEvent::Scroll(scroll)); } } } @@ -994,11 +965,11 @@ impl Entity for Terminal { type Event = Event; } -// fn make_selection(from: Point, to: Point) -> Selection { -// let mut focus = Selection::new(SelectionType::Simple, from, Direction::Left); -// focus.update(to, Direction::Right); -// focus -// } +fn make_selection(range: &RangeInclusive) -> 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. @@ -1013,7 +984,7 @@ fn make_search_matches<'a, T>( start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); - RegexIter::new(start, end, Direction::Right, term, regex) + 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) } diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 204ff241129e89e52c90d7f8b2c10109554f2522..f6d60bd964878a3fffdc50b14195f186567984af 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -7,7 +7,7 @@ use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; -use workspace::searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}; +use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; use workspace::{Item, Workspace}; use crate::TerminalSize; @@ -340,11 +340,19 @@ impl Item for TerminalContainer { impl SearchableItem for TerminalContainer { type Match = RangeInclusive; + 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 { match event { Event::Wakeup => Some(SearchEvent::MatchesInvalidated), - //TODO selection changed + Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), _ => None, } } @@ -380,25 +388,13 @@ impl SearchableItem for TerminalContainer { } } - /// Given an index, a set of matches for this index, and a direction, - /// get the next match (clicking the arrow) - fn activate_next_match( - &mut self, - _index: usize, - _direction: Direction, - _matches: Vec, - _cx: &mut ViewContext, - ) { - // TODO: - } - /// Focus match at given index into the Vec of matches - fn activate_match_at_index( - &mut self, - _index: usize, - _matches: Vec, - _cx: &mut ViewContext, - ) { + fn activate_match(&mut self, index: usize, _: Vec, cx: &mut ViewContext) { + 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 @@ -419,10 +415,27 @@ impl SearchableItem for TerminalContainer { fn active_match_index( &mut self, matches: Vec, - _cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option { - if matches.len() > 0 { - Some(0) + 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 } diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index c5dc80d62b88d883b5d81ad1249c105436dbf29c..6c6d95aacd86543c4c039e5a63b9c31b3570b570 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -579,12 +579,12 @@ impl Element for TerminalElement { // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); - if let Some(selection) = selection { - relative_highlighted_ranges.push((selection.start..=selection.end, selection_color)); - } 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 diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index de0d4f774a832e1c393be3691d23fcbfe9f0ccce..f566d1136e58e2e658d176bd15919ffa5bee3189 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -14,32 +14,64 @@ pub enum SearchEvent { 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; fn clear_matches(&mut self, cx: &mut ViewContext); fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; - fn activate_next_match( + fn activate_match( &mut self, index: usize, - direction: Direction, matches: Vec, cx: &mut ViewContext, ); - fn activate_match_at_index( + fn match_index_for_direction( &mut self, - index: usize, - matches: Vec, - cx: &mut ViewContext, - ); + matches: &Vec, + mut current_index: usize, + direction: Direction, + _: &mut ViewContext, + ) -> 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, @@ -55,28 +87,29 @@ pub trait SearchableItem: Item { pub trait SearchableItemHandle: ItemHandle { fn downgrade(&self) -> Box; fn boxed_clone(&self) -> Box; + fn supported_options(&self) -> SearchOptions; fn subscribe( &self, cx: &mut MutableAppContext, handler: Box, ) -> Subscription; - fn clear_highlights(&self, cx: &mut MutableAppContext); - fn highlight_matches(&self, matches: &Vec>, cx: &mut MutableAppContext); + fn clear_matches(&self, cx: &mut MutableAppContext); + fn update_matches(&self, matches: &Vec>, 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>, cx: &mut MutableAppContext, ); - fn select_match_by_index( + fn match_index_for_direction( &self, - index: usize, matches: &Vec>, + current_index: usize, + direction: Direction, cx: &mut MutableAppContext, - ); - fn matches( + ) -> usize; + fn find_matches( &self, query: SearchQuery, cx: &mut MutableAppContext, @@ -97,6 +130,10 @@ impl SearchableItemHandle for ViewHandle { Box::new(self.clone()) } + fn supported_options(&self) -> SearchOptions { + T::supported_options() + } + fn subscribe( &self, cx: &mut MutableAppContext, @@ -109,40 +146,38 @@ impl SearchableItemHandle for ViewHandle { }) } - fn clear_highlights(&self, cx: &mut MutableAppContext) { + fn clear_matches(&self, cx: &mut MutableAppContext) { self.update(cx, |this, cx| this.clear_matches(cx)); } - fn highlight_matches(&self, matches: &Vec>, cx: &mut MutableAppContext) { + fn update_matches(&self, matches: &Vec>, cx: &mut MutableAppContext) { let matches = downcast_matches(matches); 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>, cx: &mut MutableAppContext, ) { let matches = downcast_matches(matches); - self.update(cx, |this, cx| { - this.activate_next_match(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>, + current_index: usize, + direction: Direction, cx: &mut MutableAppContext, - ) { + ) -> usize { let matches = downcast_matches(matches); self.update(cx, |this, cx| { - this.activate_match_at_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,