Allow splitting project find and maintain the searches in sync

This commit is contained in:
Antonio Scandurra 2022-02-25 16:20:02 +01:00
parent 29e035a70d
commit 88bfe5acb0
2 changed files with 107 additions and 20 deletions

View File

@ -30,6 +30,7 @@ pub fn init(cx: &mut MutableAppContext) {
struct ProjectFind { struct ProjectFind {
project: ModelHandle<Project>, project: ModelHandle<Project>,
excerpts: ModelHandle<MultiBuffer>, excerpts: ModelHandle<MultiBuffer>,
query: Option<SearchQuery>,
pending_search: Option<Task<Option<()>>>, pending_search: Option<Task<Option<()>>>,
highlighted_ranges: Vec<Range<Anchor>>, highlighted_ranges: Vec<Range<Anchor>>,
} }
@ -55,7 +56,8 @@ impl ProjectFind {
Self { Self {
project, project,
excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
pending_search: None, query: Default::default(),
pending_search: Default::default(),
highlighted_ranges: Default::default(), highlighted_ranges: Default::default(),
} }
} }
@ -63,7 +65,8 @@ impl ProjectFind {
fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) { fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
let search = self let search = self
.project .project
.update(cx, |project, cx| project.search(query, cx)); .update(cx, |project, cx| project.search(query.clone(), cx));
self.query = Some(query.clone());
self.highlighted_ranges.clear(); self.highlighted_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; let matches = search.await;
@ -106,7 +109,7 @@ impl workspace::Item for ProjectFind {
) -> Self::View { ) -> Self::View {
let settings = workspace.settings(); let settings = workspace.settings();
let excerpts = model.read(cx).excerpts.clone(); let excerpts = model.read(cx).excerpts.clone();
cx.observe(&model, ProjectFindView::on_model_changed) cx.observe(&model, |this, _, cx| this.model_changed(true, cx))
.detach(); .detach();
ProjectFindView { ProjectFindView {
model, model,
@ -236,6 +239,36 @@ impl workspace::ItemView for ProjectFindView {
) -> Task<anyhow::Result<()>> { ) -> Task<anyhow::Result<()>> {
unreachable!("save_as should not have been called") unreachable!("save_as should not have been called")
} }
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
let query_editor = cx.add_view(|cx| {
Editor::single_line(
self.settings.clone(),
Some(|theme| theme.find.editor.input.clone()),
cx,
)
});
let results_editor = self.results_editor.update(cx, |editor, cx| {
cx.add_view(|cx| editor.clone_on_split(cx).unwrap())
});
cx.observe(&self.model, |this, _, cx| this.model_changed(true, cx))
.detach();
let mut view = Self {
model: self.model.clone(),
query_editor,
results_editor,
case_sensitive: self.case_sensitive,
whole_word: self.whole_word,
regex: self.regex,
query_contains_error: self.query_contains_error,
settings: self.settings.clone(),
};
view.model_changed(false, cx);
Some(view)
}
} }
impl ProjectFindView { impl ProjectFindView {
@ -247,7 +280,7 @@ impl ProjectFindView {
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) { fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
let text = self.query_editor.read(cx).text(cx); let text = self.query_editor.read(cx).text(cx);
let query = if self.regex { let query = if self.regex {
match SearchQuery::regex(text, self.case_sensitive, self.whole_word) { match SearchQuery::regex(text, self.whole_word, self.case_sensitive) {
Ok(query) => query, Ok(query) => query,
Err(_) => { Err(_) => {
self.query_contains_error = true; self.query_contains_error = true;
@ -256,7 +289,7 @@ impl ProjectFindView {
} }
} }
} else { } else {
SearchQuery::text(text, self.case_sensitive, self.whole_word) SearchQuery::text(text, self.whole_word, self.case_sensitive)
}; };
self.model.update(cx, |model, cx| model.search(query, cx)); self.model.update(cx, |model, cx| model.search(query, cx));
@ -285,16 +318,36 @@ impl ProjectFindView {
} }
} }
fn on_model_changed(&mut self, _: ModelHandle<ProjectFind>, 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 model = self.model.read(cx);
let highlighted_ranges = model.highlighted_ranges.clone();
if let Some(query) = model.query.clone() {
self.case_sensitive = query.case_sensitive();
self.whole_word = query.whole_word();
self.regex = query.is_regex();
self.query_editor.update(cx, |query_editor, cx| {
if query_editor.text(cx) != query.as_str() {
query_editor.buffer().update(cx, |query_buffer, cx| {
let len = query_buffer.read(cx).len();
query_buffer.edit([0..len], query.as_str(), cx);
});
}
});
}
if !highlighted_ranges.is_empty() { if !highlighted_ranges.is_empty() {
let theme = &self.settings.borrow().theme.find; let theme = &self.settings.borrow().theme.find;
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>(highlighted_ranges, theme.match_background, cx);
if reset_selections {
editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); editor.select_ranges([0..0], Some(Autoscroll::Fit), cx);
}
}); });
if self.query_editor.is_focused(cx) {
cx.focus(&self.results_editor); cx.focus(&self.results_editor);
} }
}
cx.notify(); cx.notify();
} }

View File

@ -13,12 +13,16 @@ use std::{
pub enum SearchQuery { pub enum SearchQuery {
Text { Text {
search: Arc<AhoCorasick<usize>>, search: Arc<AhoCorasick<usize>>,
query: String, query: Arc<str>,
whole_word: bool, whole_word: bool,
case_sensitive: bool,
}, },
Regex { Regex {
multiline: bool,
regex: Regex, regex: Regex,
query: Arc<str>,
multiline: bool,
whole_word: bool,
case_sensitive: bool,
}, },
} }
@ -31,13 +35,15 @@ impl SearchQuery {
.build(&[&query]); .build(&[&query]);
Self::Text { Self::Text {
search: Arc::new(search), search: Arc::new(search),
query, query: Arc::from(query),
whole_word, whole_word,
case_sensitive,
} }
} }
pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result<Self> { pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result<Self> {
let mut query = query.to_string(); let mut query = query.to_string();
let initial_query = Arc::from(query.as_str());
if whole_word { if whole_word {
let mut word_query = String::new(); let mut word_query = String::new();
word_query.push_str("\\b"); word_query.push_str("\\b");
@ -51,7 +57,13 @@ impl SearchQuery {
.case_insensitive(!case_sensitive) .case_insensitive(!case_sensitive)
.multi_line(multiline) .multi_line(multiline)
.build()?; .build()?;
Ok(Self::Regex { multiline, regex }) Ok(Self::Regex {
regex,
query: initial_query,
multiline,
whole_word,
case_sensitive,
})
} }
pub fn detect<T: Read>(&self, stream: T) -> Result<bool> { pub fn detect<T: Read>(&self, stream: T) -> Result<bool> {
@ -60,7 +72,7 @@ impl SearchQuery {
} }
match self { match self {
SearchQuery::Text { search, .. } => { Self::Text { search, .. } => {
let mat = search.stream_find_iter(stream).next(); let mat = search.stream_find_iter(stream).next();
match mat { match mat {
Some(Ok(_)) => Ok(true), Some(Ok(_)) => Ok(true),
@ -68,7 +80,9 @@ impl SearchQuery {
None => Ok(false), None => Ok(false),
} }
} }
SearchQuery::Regex { multiline, regex } => { Self::Regex {
regex, multiline, ..
} => {
let mut reader = BufReader::new(stream); let mut reader = BufReader::new(stream);
if *multiline { if *multiline {
let mut text = String::new(); let mut text = String::new();
@ -99,7 +113,7 @@ impl SearchQuery {
let mut matches = Vec::new(); let mut matches = Vec::new();
match self { match self {
SearchQuery::Text { Self::Text {
search, whole_word, .. search, whole_word, ..
} => { } => {
for (ix, mat) in search for (ix, mat) in search
@ -123,7 +137,9 @@ impl SearchQuery {
matches.push(mat.start()..mat.end()) matches.push(mat.start()..mat.end())
} }
} }
SearchQuery::Regex { multiline, regex } => { Self::Regex {
regex, multiline, ..
} => {
if *multiline { if *multiline {
let text = rope.to_string(); let text = rope.to_string();
for (ix, mat) in regex.find_iter(&text).enumerate() { for (ix, mat) in regex.find_iter(&text).enumerate() {
@ -161,10 +177,28 @@ impl SearchQuery {
matches matches
} }
fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
SearchQuery::Text { query, .. } => query.as_str(), Self::Text { query, .. } => query.as_ref(),
SearchQuery::Regex { regex, .. } => regex.as_str(), Self::Regex { query, .. } => query.as_ref(),
} }
} }
pub fn whole_word(&self) -> bool {
match self {
Self::Text { whole_word, .. } => *whole_word,
Self::Regex { whole_word, .. } => *whole_word,
}
}
pub fn case_sensitive(&self) -> bool {
match self {
Self::Text { case_sensitive, .. } => *case_sensitive,
Self::Regex { case_sensitive, .. } => *case_sensitive,
}
}
pub fn is_regex(&self) -> bool {
matches!(self, Self::Regex { .. })
}
} }