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 collections::HashMap;
|
||||||
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
|
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -18,13 +18,6 @@ action!(Deploy, bool);
|
||||||
action!(Dismiss);
|
action!(Dismiss);
|
||||||
action!(FocusEditor);
|
action!(FocusEditor);
|
||||||
action!(ToggleSearchOption, SearchOption);
|
action!(ToggleSearchOption, SearchOption);
|
||||||
action!(SelectMatch, Direction);
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Direction {
|
|
||||||
Prev,
|
|
||||||
Next,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(cx: &mut MutableAppContext) {
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
cx.add_bindings([
|
cx.add_bindings([
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::SearchOption;
|
use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption};
|
||||||
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
|
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, SelectNext};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
|
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
|
||||||
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
|
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
|
||||||
|
@ -9,6 +9,7 @@ use postage::watch;
|
||||||
use project::{search::SearchQuery, Project};
|
use project::{search::SearchQuery, Project};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
cmp::{self, Ordering},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
@ -18,7 +19,6 @@ use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}
|
||||||
action!(Deploy);
|
action!(Deploy);
|
||||||
action!(Search);
|
action!(Search);
|
||||||
action!(SearchInNew);
|
action!(SearchInNew);
|
||||||
action!(ToggleSearchOption, SearchOption);
|
|
||||||
action!(ToggleFocus);
|
action!(ToggleFocus);
|
||||||
|
|
||||||
const MAX_TAB_TITLE_LEN: usize = 24;
|
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("cmd-shift-F", Deploy, Some("Workspace")),
|
||||||
Binding::new("enter", Search, Some("ProjectSearchView")),
|
Binding::new("enter", Search, Some("ProjectSearchView")),
|
||||||
Binding::new("cmd-enter", SearchInNew, 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::deploy);
|
||||||
cx.add_action(ProjectSearchView::search);
|
cx.add_action(ProjectSearchView::search);
|
||||||
cx.add_action(ProjectSearchView::search_in_new);
|
cx.add_action(ProjectSearchView::search_in_new);
|
||||||
cx.add_action(ProjectSearchView::toggle_search_option);
|
cx.add_action(ProjectSearchView::toggle_search_option);
|
||||||
cx.add_action(ProjectSearchView::toggle_focus);
|
cx.add_action(ProjectSearchView::toggle_focus);
|
||||||
|
cx.add_action(ProjectSearchView::select_match);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProjectSearch {
|
struct ProjectSearch {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
excerpts: ModelHandle<MultiBuffer>,
|
excerpts: ModelHandle<MultiBuffer>,
|
||||||
pending_search: Option<Task<Option<()>>>,
|
pending_search: Option<Task<Option<()>>>,
|
||||||
highlighted_ranges: Vec<Range<Anchor>>,
|
match_ranges: Vec<Range<Anchor>>,
|
||||||
active_query: Option<SearchQuery>,
|
active_query: Option<SearchQuery>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +65,7 @@ struct ProjectSearchView {
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
regex: bool,
|
regex: bool,
|
||||||
query_contains_error: bool,
|
query_contains_error: bool,
|
||||||
|
active_match_index: Option<usize>,
|
||||||
settings: watch::Receiver<Settings>,
|
settings: watch::Receiver<Settings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +80,7 @@ impl ProjectSearch {
|
||||||
project,
|
project,
|
||||||
excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
|
excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
|
||||||
pending_search: Default::default(),
|
pending_search: Default::default(),
|
||||||
highlighted_ranges: Default::default(),
|
match_ranges: Default::default(),
|
||||||
active_query: None,
|
active_query: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +92,7 @@ impl ProjectSearch {
|
||||||
.excerpts
|
.excerpts
|
||||||
.update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
|
.update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
|
||||||
pending_search: Default::default(),
|
pending_search: Default::default(),
|
||||||
highlighted_ranges: self.highlighted_ranges.clone(),
|
match_ranges: self.match_ranges.clone(),
|
||||||
active_query: self.active_query.clone(),
|
active_query: self.active_query.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -90,12 +102,12 @@ impl ProjectSearch {
|
||||||
.project
|
.project
|
||||||
.update(cx, |project, cx| project.search(query.clone(), cx));
|
.update(cx, |project, cx| project.search(query.clone(), cx));
|
||||||
self.active_query = Some(query);
|
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 {
|
self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||||
let matches = search.await.log_err()?;
|
let matches = search.await.log_err()?;
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.highlighted_ranges.clear();
|
this.match_ranges.clear();
|
||||||
let mut matches = matches.into_iter().collect::<Vec<_>>();
|
let mut matches = matches.into_iter().collect::<Vec<_>>();
|
||||||
matches
|
matches
|
||||||
.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
|
.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path()));
|
||||||
|
@ -108,7 +120,7 @@ impl ProjectSearch {
|
||||||
1,
|
1,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
this.highlighted_ranges.extend(ranges_to_highlight);
|
this.match_ranges.extend(ranges_to_highlight);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.pending_search.take();
|
this.pending_search.take();
|
||||||
|
@ -153,7 +165,7 @@ impl View for ProjectSearchView {
|
||||||
|
|
||||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
let model = &self.model.read(cx);
|
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 theme = &self.settings.borrow().theme;
|
||||||
let text = if self.query_editor.read(cx).text(cx).is_empty() {
|
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>) {
|
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);
|
cx.focus(&self.query_editor);
|
||||||
} else {
|
} else {
|
||||||
self.focus_results_editor(cx);
|
self.focus_results_editor(cx);
|
||||||
|
@ -348,6 +360,12 @@ impl ProjectSearchView {
|
||||||
});
|
});
|
||||||
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.subscribe(&results_editor, |this, _, event, cx| {
|
||||||
|
if matches!(event, editor::Event::SelectionsChanged) {
|
||||||
|
this.update_match_index(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
let mut this = ProjectSearchView {
|
let mut this = ProjectSearchView {
|
||||||
model,
|
model,
|
||||||
|
@ -357,6 +375,7 @@ impl ProjectSearchView {
|
||||||
whole_word,
|
whole_word,
|
||||||
regex,
|
regex,
|
||||||
query_contains_error: false,
|
query_contains_error: false,
|
||||||
|
active_match_index: None,
|
||||||
settings,
|
settings,
|
||||||
};
|
};
|
||||||
this.model_changed(false, cx);
|
this.model_changed(false, cx);
|
||||||
|
@ -446,9 +465,52 @@ impl ProjectSearchView {
|
||||||
cx.notify();
|
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>) {
|
fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
|
||||||
if self.query_editor.is_focused(cx) {
|
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);
|
self.focus_results_editor(cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -461,18 +523,20 @@ impl ProjectSearchView {
|
||||||
|
|
||||||
fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
|
fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
|
||||||
self.query_editor.update(cx, |query_editor, cx| {
|
self.query_editor.update(cx, |query_editor, cx| {
|
||||||
let head = query_editor.newest_anchor_selection().head();
|
let cursor = query_editor.newest_anchor_selection().head();
|
||||||
query_editor.select_ranges([head.clone()..head], None, cx);
|
query_editor.select_ranges([cursor.clone()..cursor], None, cx);
|
||||||
});
|
});
|
||||||
cx.focus(&self.results_editor);
|
cx.focus(&self.results_editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext<Self>) {
|
fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext<Self>) {
|
||||||
let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone();
|
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||||
if !highlighted_ranges.is_empty() {
|
if match_ranges.is_empty() {
|
||||||
|
self.active_match_index = None;
|
||||||
|
} else {
|
||||||
let theme = &self.settings.borrow().theme.search;
|
let theme = &self.settings.borrow().theme.search;
|
||||||
self.results_editor.update(cx, |editor, cx| {
|
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 {
|
if reset_selections {
|
||||||
editor.select_ranges([0..0], Some(Autoscroll::Fit), cx);
|
editor.select_ranges([0..0], Some(Autoscroll::Fit), cx);
|
||||||
}
|
}
|
||||||
|
@ -486,6 +550,34 @@ impl ProjectSearchView {
|
||||||
cx.notify();
|
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 {
|
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
let theme = &self.settings.borrow().theme;
|
let theme = &self.settings.borrow().theme;
|
||||||
let editor_container = if self.query_contains_error {
|
let editor_container = if self.query_contains_error {
|
||||||
|
@ -513,6 +605,29 @@ impl ProjectSearchView {
|
||||||
.aligned()
|
.aligned()
|
||||||
.boxed(),
|
.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()
|
.contained()
|
||||||
.with_style(theme.search.container)
|
.with_style(theme.search.container)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
@ -552,4 +667,28 @@ impl ProjectSearchView {
|
||||||
SearchOption::Regex => self.regex,
|
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 buffer_search;
|
||||||
mod project_search;
|
mod project_search;
|
||||||
|
@ -8,9 +8,18 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
project_search::init(cx);
|
project_search::init(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action!(ToggleSearchOption, SearchOption);
|
||||||
|
action!(SelectMatch, Direction);
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum SearchOption {
|
pub enum SearchOption {
|
||||||
WholeWord,
|
WholeWord,
|
||||||
CaseSensitive,
|
CaseSensitive,
|
||||||
Regex,
|
Regex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Direction {
|
||||||
|
Prev,
|
||||||
|
Next,
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue