Implement navigation between project search matches
This commit is contained in:
parent
7ef98fb935
commit
64d22925c2
3 changed files with 167 additions and 26 deletions
|
@ -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([
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue