Implement navigation between project search matches

This commit is contained in:
Nathan Sobo 2022-02-27 14:18:04 -07:00
parent 7ef98fb935
commit 64d22925c2
3 changed files with 167 additions and 26 deletions

View file

@ -1,4 +1,4 @@
use crate::SearchOption;
use crate::{Direction, SearchOption, SelectMatch};
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
use gpui::{
@ -18,13 +18,6 @@ action!(Deploy, bool);
action!(Dismiss);
action!(FocusEditor);
action!(ToggleSearchOption, SearchOption);
action!(SelectMatch, Direction);
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings([

View file

@ -1,5 +1,5 @@
use crate::SearchOption;
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption};
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, SelectNext};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
@ -9,6 +9,7 @@ use postage::watch;
use project::{search::SearchQuery, Project};
use std::{
any::{Any, TypeId},
cmp::{self, Ordering},
ops::Range,
path::PathBuf,
};
@ -18,7 +19,6 @@ use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}
action!(Deploy);
action!(Search);
action!(SearchInNew);
action!(ToggleSearchOption, SearchOption);
action!(ToggleFocus);
const MAX_TAB_TITLE_LEN: usize = 24;
@ -30,19 +30,30 @@ pub fn init(cx: &mut MutableAppContext) {
Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
Binding::new("enter", Search, Some("ProjectSearchView")),
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")),
Binding::new(
"cmd-g",
SelectMatch(Direction::Next),
Some("ProjectSearchView"),
),
Binding::new(
"cmd-shift-G",
SelectMatch(Direction::Prev),
Some("ProjectSearchView"),
),
]);
cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchView::search);
cx.add_action(ProjectSearchView::search_in_new);
cx.add_action(ProjectSearchView::toggle_search_option);
cx.add_action(ProjectSearchView::toggle_focus);
cx.add_action(ProjectSearchView::select_match);
}
struct ProjectSearch {
project: ModelHandle<Project>,
excerpts: ModelHandle<MultiBuffer>,
pending_search: Option<Task<Option<()>>>,
highlighted_ranges: Vec<Range<Anchor>>,
match_ranges: Vec<Range<Anchor>>,
active_query: Option<SearchQuery>,
}
@ -54,6 +65,7 @@ struct ProjectSearchView {
whole_word: bool,
regex: bool,
query_contains_error: bool,
active_match_index: Option<usize>,
settings: watch::Receiver<Settings>,
}
@ -68,7 +80,7 @@ impl ProjectSearch {
project,
excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
pending_search: Default::default(),
highlighted_ranges: Default::default(),
match_ranges: Default::default(),
active_query: None,
}
}
@ -80,7 +92,7 @@ impl ProjectSearch {
.excerpts
.update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
pending_search: Default::default(),
highlighted_ranges: self.highlighted_ranges.clone(),
match_ranges: self.match_ranges.clone(),
active_query: self.active_query.clone(),
})
}
@ -90,12 +102,12 @@ impl ProjectSearch {
.project
.update(cx, |project, cx| project.search(query.clone(), cx));
self.active_query = Some(query);
self.highlighted_ranges.clear();
self.match_ranges.clear();
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
let matches = search.await.log_err()?;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.highlighted_ranges.clear();
this.match_ranges.clear();
let mut matches = matches.into_iter().collect::<Vec<_>>();
matches
.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
@ -108,7 +120,7 @@ impl ProjectSearch {
1,
cx,
);
this.highlighted_ranges.extend(ranges_to_highlight);
this.match_ranges.extend(ranges_to_highlight);
}
});
this.pending_search.take();
@ -153,7 +165,7 @@ impl View for ProjectSearchView {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx);
let results = if model.highlighted_ranges.is_empty() {
let results = if model.match_ranges.is_empty() {
let theme = &self.settings.borrow().theme;
let text = if self.query_editor.read(cx).text(cx).is_empty() {
""
@ -181,7 +193,7 @@ impl View for ProjectSearchView {
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
if self.model.read(cx).highlighted_ranges.is_empty() {
if self.model.read(cx).match_ranges.is_empty() {
cx.focus(&self.query_editor);
} else {
self.focus_results_editor(cx);
@ -348,6 +360,12 @@ impl ProjectSearchView {
});
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
.detach();
cx.subscribe(&results_editor, |this, _, event, cx| {
if matches!(event, editor::Event::SelectionsChanged) {
this.update_match_index(cx);
}
})
.detach();
let mut this = ProjectSearchView {
model,
@ -357,6 +375,7 @@ impl ProjectSearchView {
whole_word,
regex,
query_contains_error: false,
active_match_index: None,
settings,
};
this.model_changed(false, cx);
@ -446,9 +465,52 @@ impl ProjectSearchView {
cx.notify();
}
fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
if let Some(mut index) = self.active_match_index {
let range_to_select = {
let model = self.model.read(cx);
let results_editor = self.results_editor.read(cx);
let buffer = results_editor.buffer().read(cx).read(cx);
let cursor = results_editor.newest_anchor_selection().head();
let ranges = &model.match_ranges;
if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() {
if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
}
} else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() {
if direction == Direction::Next {
index = 0;
}
} else if direction == Direction::Prev {
if index == 0 {
index = ranges.len() - 1;
} else {
index -= 1;
}
} else if direction == Direction::Next {
if index == ranges.len() - 1 {
index = 0
} else {
index += 1;
}
};
ranges[index].clone()
};
self.results_editor.update(cx, |editor, cx| {
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
});
}
}
fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).highlighted_ranges.is_empty() {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
@ -461,18 +523,20 @@ impl ProjectSearchView {
fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
let head = query_editor.newest_anchor_selection().head();
query_editor.select_ranges([head.clone()..head], None, cx);
let cursor = query_editor.newest_anchor_selection().head();
query_editor.select_ranges([cursor.clone()..cursor], None, cx);
});
cx.focus(&self.results_editor);
}
fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext<Self>) {
let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone();
if !highlighted_ranges.is_empty() {
let match_ranges = self.model.read(cx).match_ranges.clone();
if match_ranges.is_empty() {
self.active_match_index = None;
} else {
let theme = &self.settings.borrow().theme.search;
self.results_editor.update(cx, |editor, cx| {
editor.highlight_ranges::<Self>(highlighted_ranges, theme.match_background, cx);
editor.highlight_ranges::<Self>(match_ranges, theme.match_background, cx);
if reset_selections {
editor.select_ranges([0..0], Some(Autoscroll::Fit), cx);
}
@ -486,6 +550,34 @@ impl ProjectSearchView {
cx.notify();
}
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
let match_ranges = self.model.read(cx).match_ranges.clone();
if match_ranges.is_empty() {
self.active_match_index = None;
} else {
let results_editor = &self.results_editor.read(cx);
let cursor = results_editor.newest_anchor_selection().head();
let new_index = {
let buffer = results_editor.buffer().read(cx).read(cx);
match match_ranges.binary_search_by(|probe| {
if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() {
Ordering::Less
} else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() {
Ordering::Greater
} else {
Ordering::Equal
}
}) {
Ok(i) | Err(i) => Some(cmp::min(i, match_ranges.len() - 1)),
}
};
if self.active_match_index != new_index {
self.active_match_index = new_index;
cx.notify();
}
}
}
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &self.settings.borrow().theme;
let editor_container = if self.query_contains_error {
@ -513,6 +605,29 @@ impl ProjectSearchView {
.aligned()
.boxed(),
)
.with_children({
self.active_match_index.into_iter().flat_map(|match_ix| {
[
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
Label::new(
format!(
"{}/{}",
match_ix + 1,
self.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
]
})
})
.contained()
.with_style(theme.search.container)
.constrained()
@ -552,4 +667,28 @@ impl ProjectSearchView {
SearchOption::Regex => self.regex,
}
}
fn render_nav_button(
&self,
icon: &str,
direction: Direction,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let theme = &self.settings.borrow().theme.search;
enum NavButton {}
MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, _| {
let style = if state.hovered {
&theme.hovered_option_button
} else {
&theme.option_button
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(SelectMatch(direction)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
}

View file

@ -1,4 +1,4 @@
use gpui::MutableAppContext;
use gpui::{action, MutableAppContext};
mod buffer_search;
mod project_search;
@ -8,9 +8,18 @@ pub fn init(cx: &mut MutableAppContext) {
project_search::init(cx);
}
action!(ToggleSearchOption, SearchOption);
action!(SelectMatch, Direction);
#[derive(Clone, Copy)]
pub enum SearchOption {
WholeWord,
CaseSensitive,
Regex,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Prev,
Next,
}