@@ -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<D> {
- self.resolve_selection(self.newest_selection_internal().unwrap(), snapshot)
+ self.resolve_selection(self.newest_anchor_selection().unwrap(), snapshot)
}
- pub fn newest_selection_internal(&self) -> Option<&Selection<Anchor>> {
+ pub fn newest_anchor_selection(&self) -> Option<&Selection<Anchor>> {
self.pending_selection
.as_ref()
.map(|s| &s.selection)
@@ -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<Self>) {
@@ -3751,7 +3752,7 @@ impl Editor {
}
#[cfg(feature = "test-support")]
- pub fn highlighted_ranges(
+ pub fn all_highlighted_ranges(
&mut self,
cx: &mut ViewContext<Self>,
) -> Vec<(Range<DisplayPoint>, Color)> {
@@ -3762,6 +3763,12 @@ impl Editor {
self.highlighted_ranges_in_range(start..end, &snapshot)
}
+ pub fn highlighted_ranges_for_type<T: 'static>(&self) -> Option<(Color, &[Range<Anchor>])> {
+ self.highlighted_ranges
+ .get(&TypeId::of::<T>())
+ .map(|(color, ranges)| (*color, ranges.as_slice()))
+ }
+
pub fn highlighted_ranges_in_range(
&self,
search_range: Range<Anchor>,
@@ -4011,6 +4018,7 @@ pub enum Event {
Dirtied,
Saved,
TitleChanged,
+ SelectionsChanged,
Closed,
}
@@ -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<Settings>,
query_editor: ViewHandle<Editor>,
active_editor: Option<ViewHandle<Editor>>,
+ active_match_index: Option<usize>,
active_editor_subscription: Option<Subscription>,
highlighted_editors: HashSet<WeakViewHandle<Editor>>,
pending_search: Option<Task<()>>,
@@ -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<Self>,
) -> ElementBox {
let is_active = self.is_mode_enabled(mode);
- MouseEventHandler::new::<Self, _, _, _>(mode as usize, cx, |state, _| {
+ MouseEventHandler::new::<Self, _, _, _>((cx.view_id(), mode as usize), cx, |state, _| {
let style = match (is_active, state.hovered) {
(false, false) => &theme.mode_button,
(false, true) => &theme.hovered_mode_button,
@@ -182,6 +198,32 @@ impl FindBar {
.boxed()
}
+ fn render_nav_button(
+ &self,
+ icon: &str,
+ direction: Direction,
+ theme: &theme::Find,
+ cx: &mut RenderContext<Self>,
+ ) -> ElementBox {
+ MouseEventHandler::new::<Self, _, _, _>(
+ (cx.view_id(), 10 + direction as usize),
+ cx,
+ |state, _| {
+ let style = if state.hovered {
+ &theme.hovered_mode_button
+ } else {
+ &theme.mode_button
+ };
+ Label::new(icon.to_string(), style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ },
+ )
+ .on_click(move |cx| cx.dispatch_action(GoToMatch(direction)))
+ .boxed()
+ }
+
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
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<Self>) {
+ 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::<Self>() {
+ 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<Editor>,
@@ -244,12 +312,14 @@ impl FindBar {
fn on_active_editor_event(
&mut self,
- _: ViewHandle<Editor>,
+ editor: ViewHandle<Editor>,
event: &editor::Event,
cx: &mut ViewContext<Self>,
) {
match event {
editor::Event::Edited => self.update_matches(cx),
+ editor::Event::SelectionsChanged => self.update_match_index(cx),
+
_ => {}
}
}
@@ -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::<Self>(
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>) {
+ self.active_match_index = self.active_match_index(cx);
+ }
+
+ fn active_match_index(&mut self, cx: &mut ViewContext<Self>) -> Option<usize> {
+ let editor = self.active_editor.as_ref()?;
+ let editor = editor.read(cx);
+ let position = editor.newest_anchor_selection()?.head();
+ let ranges = editor.highlighted_ranges_for_type::<Self>()?.1;
+ 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);
+ });
}
}