diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5b04e129c4c59844a0127c10daab0dae42be55bc..425d500d3d59cd9b5fcb02c1f51924166a66eede 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -861,7 +861,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; - let newest_selection = self.newest_selection_internal().unwrap().clone(); + let newest_selection = self.newest_anchor_selection().unwrap().clone(); let start; let end; @@ -3358,10 +3358,10 @@ impl Editor { &self, snapshot: &MultiBufferSnapshot, ) -> Selection { - self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot) + self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot) } - pub fn newest_selection_internal(&self) -> Option<&Selection> { + pub fn newest_anchor_selection(&self) -> Option<&Selection> { self.pending_selection .as_ref() .map(|s| &s.selection) @@ -3377,7 +3377,7 @@ impl Editor { T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, { let buffer = self.buffer.read(cx).snapshot(cx); - let old_cursor_position = self.newest_selection_internal().map(|s| s.head()); + let old_cursor_position = self.newest_anchor_selection().map(|s| s.head()); selections.sort_unstable_by_key(|s| s.start); // Merge overlapping selections. @@ -3511,6 +3511,7 @@ impl Editor { buffer.set_active_selections(&self.selections, cx) }); } + cx.emit(Event::SelectionsChanged); } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { @@ -3751,7 +3752,7 @@ impl Editor { } #[cfg(feature = "test-support")] - pub fn highlighted_ranges( + pub fn all_highlighted_ranges( &mut self, cx: &mut ViewContext, ) -> Vec<(Range, Color)> { @@ -3762,6 +3763,12 @@ impl Editor { self.highlighted_ranges_in_range(start..end, &snapshot) } + pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { + self.highlighted_ranges + .get(&TypeId::of::()) + .map(|(color, ranges)| (*color, ranges.as_slice())) + } + pub fn highlighted_ranges_in_range( &self, search_range: Range, @@ -4011,6 +4018,7 @@ pub enum Event { Dirtied, Saved, TitleChanged, + SelectionsChanged, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1dafec32c6f080dfe2799513840c14ad26eff28e..97ce05615246cdd236e4a32cf580296bd142d119 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -141,7 +141,7 @@ impl ItemView for Editor { } fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(selection) = self.newest_selection_internal() { + if let Some(selection) = self.newest_anchor_selection() { self.push_to_nav_history(selection.head(), None, cx); } } diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index db2d74a0a9b64cdbcbfbac135ad03ac137e4e2c4..434056b9a96d632f458ea3ba70378aa856610e95 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -9,12 +9,19 @@ use gpui::{ use postage::watch; use regex::RegexBuilder; use smol::future::yield_now; -use std::{ops::Range, sync::Arc}; +use std::{cmp::Ordering, ops::Range, sync::Arc}; use workspace::{ItemViewHandle, Settings, Toolbar, Workspace}; action!(Deploy); action!(Cancel); action!(ToggleMode, SearchMode); +action!(GoToMatch, Direction); + +#[derive(Clone, Copy)] +pub enum Direction { + Prev, + Next, +} #[derive(Clone, Copy)] pub enum SearchMode { @@ -31,12 +38,14 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(FindBar::deploy); cx.add_action(FindBar::cancel); cx.add_action(FindBar::toggle_mode); + cx.add_action(FindBar::go_to_match); } struct FindBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, + active_match_index: Option, active_editor_subscription: Option, highlighted_editors: HashSet>, pending_search: Option>, @@ -84,6 +93,12 @@ impl View for FindBar { .with_style(theme.mode_button_group) .boxed(), ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, theme, cx)) + .with_child(self.render_nav_button(">", Direction::Next, theme, cx)) + .boxed(), + ) .contained() .with_style(theme.container) .boxed() @@ -138,6 +153,7 @@ impl FindBar { query_editor, active_editor: None, active_editor_subscription: None, + active_match_index: None, highlighted_editors: Default::default(), case_sensitive_mode: false, whole_word_mode: false, @@ -166,7 +182,7 @@ impl FindBar { cx: &mut RenderContext, ) -> ElementBox { let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(mode as usize, cx, |state, _| { + MouseEventHandler::new::((cx.view_id(), mode as usize), cx, |state, _| { let style = match (is_active, state.hovered) { (false, false) => &theme.mode_button, (false, true) => &theme.hovered_mode_button, @@ -182,6 +198,32 @@ impl FindBar { .boxed() } + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + theme: &theme::Find, + cx: &mut RenderContext, + ) -> ElementBox { + MouseEventHandler::new::( + (cx.view_id(), 10 + direction as usize), + cx, + |state, _| { + let style = if state.hovered { + &theme.hovered_mode_button + } else { + &theme.mode_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }, + ) + .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .boxed() + } + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { @@ -217,6 +259,32 @@ impl FindBar { cx.notify(); } + fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { + if let Some(mut index) = self.active_match_index { + if let Some(editor) = self.active_editor.as_ref() { + editor.update(cx, |editor, cx| { + if let Some((_, ranges)) = editor.highlighted_ranges_for_type::() { + match direction { + Direction::Prev => { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + Direction::Next => { + index += 1; + if index >= ranges.len() { + index = 0; + } + } + } + } + }); + } + } + } + fn on_query_editor_event( &mut self, _: ViewHandle, @@ -244,12 +312,14 @@ impl FindBar { fn on_active_editor_event( &mut self, - _: ViewHandle, + editor: ViewHandle, event: &editor::Event, cx: &mut ViewContext, ) { match event { editor::Event::Edited => self.update_matches(cx), + editor::Event::SelectionsChanged => self.update_match_index(cx), + _ => {} } } @@ -279,15 +349,16 @@ impl FindBar { Ok(ranges) => { if let Some(editor) = cx.read(|cx| editor.upgrade(cx)) { this.update(&mut cx, |this, cx| { - let theme = &this.settings.borrow().theme.find; this.highlighted_editors.insert(editor.downgrade()); editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.find; editor.highlight_ranges::( ranges, theme.match_background, cx, ) }); + this.update_match_index(cx); }); } } @@ -302,6 +373,29 @@ impl FindBar { } } } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + self.active_match_index = self.active_match_index(cx); + } + + fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { + let editor = self.active_editor.as_ref()?; + let editor = editor.read(cx); + let position = editor.newest_anchor_selection()?.head(); + let ranges = editor.highlighted_ranges_for_type::()?.1; + let buffer = editor.buffer().read(cx).read(cx); + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(i), + } + } } const YIELD_INTERVAL: usize = 20000; @@ -445,14 +539,15 @@ mod tests { find_bar }); - // default: case-insensitive substring search. + // Search for a string that appears with different casing. + // By default, search is case-insensitive. find_bar.update(&mut cx, |find_bar, cx| { find_bar.set_query("us", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[ ( DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), @@ -466,28 +561,30 @@ mod tests { ); }); - // switch to case sensitive search + // Switch to a case sensitive search. find_bar.update(&mut cx, |find_bar, cx| { find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), Color::red(), - ),] + )] ); }); + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. find_bar.update(&mut cx, |find_bar, cx| { find_bar.set_query("or", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[ ( DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), @@ -521,14 +618,14 @@ mod tests { ); }); - // switch to whole word search + // Switch to a whole word search. find_bar.update(&mut cx, |find_bar, cx| { find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { assert_eq!( - editor.highlighted_ranges(cx), + editor.all_highlighted_ranges(cx), &[ ( DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), @@ -545,5 +642,9 @@ mod tests { ] ); }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + }); } }