From 915154b0479e749a667d1873a6822ffaefa512cf Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 7 May 2023 22:17:26 +0300 Subject: [PATCH] Add initial include/exclude project search UI --- Cargo.lock | 2 + crates/collab/src/tests/integration_tests.rs | 5 +- .../src/tests/randomized_integration_tests.rs | 5 +- crates/project/Cargo.toml | 1 + crates/project/src/project_tests.rs | 20 ++-- crates/project/src/search.rs | 82 ++++++++++++++++- crates/rpc/proto/zed.proto | 2 + crates/search/Cargo.toml | 1 + crates/search/src/buffer_search.rs | 16 +++- crates/search/src/project_search.rs | 92 ++++++++++++++++++- 10 files changed, 212 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e268218d56..eee0873e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4719,6 +4719,7 @@ dependencies = [ "glob", "gpui", "ignore", + "itertools", "language", "lazy_static", "log", @@ -5771,6 +5772,7 @@ dependencies = [ "collections", "editor", "futures 0.3.25", + "glob", "gpui", "language", "log", diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 9f04642e30..e3b5b0be7e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4548,7 +4548,10 @@ async fn test_project_search( // Perform a search as the guest. let results = project_b .update(cx_b, |project, cx| { - project.search(SearchQuery::text("world", false, false), cx) + project.search( + SearchQuery::text("world", false, false, Vec::new(), Vec::new()), + cx, + ) }) .await .unwrap(); diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index d5bd0033f7..c4326be101 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -716,7 +716,10 @@ async fn apply_client_operation( ); let search = project.update(cx, |project, cx| { - project.search(SearchQuery::text(query, false, false), cx) + project.search( + SearchQuery::text(query, false, false, Vec::new(), Vec::new()), + cx, + ) }); drop(project); let search = cx.background().spawn(async move { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 46b00fc6ee..2b4892aab9 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -58,6 +58,7 @@ similar = "1.3" smol.workspace = true thiserror.workspace = true toml = "0.5" +itertools = "0.10" [dev-dependencies] ctor.workspace = true diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index fc530b5122..24c4ff3821 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3297,9 +3297,13 @@ async fn test_search(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; assert_eq!( - search(&project, SearchQuery::text("TWO", false, true), cx) - .await - .unwrap(), + search( + &project, + SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()), + cx + ) + .await + .unwrap(), HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]) @@ -3318,9 +3322,13 @@ async fn test_search(cx: &mut gpui::TestAppContext) { }); assert_eq!( - search(&project, SearchQuery::text("TWO", false, true), cx) - .await - .unwrap(), + search( + &project, + SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()), + cx + ) + .await + .unwrap(), HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]), diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index b526ab0c18..5d856a3b93 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,6 +1,7 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use anyhow::Result; use client::proto; +use itertools::Itertools; use language::{char_kind, Rope}; use regex::{Regex, RegexBuilder}; use smol::future::yield_now; @@ -17,6 +18,8 @@ pub enum SearchQuery { query: Arc, whole_word: bool, case_sensitive: bool, + files_to_include: Vec, + files_to_exclude: Vec, }, Regex { regex: Regex, @@ -24,11 +27,19 @@ pub enum SearchQuery { multiline: bool, whole_word: bool, case_sensitive: bool, + files_to_include: Vec, + files_to_exclude: Vec, }, } impl SearchQuery { - pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { + pub fn text( + query: impl ToString, + whole_word: bool, + case_sensitive: bool, + files_to_include: Vec, + files_to_exclude: Vec, + ) -> Self { let query = query.to_string(); let search = AhoCorasickBuilder::new() .auto_configure(&[&query]) @@ -39,10 +50,18 @@ impl SearchQuery { query: Arc::from(query), whole_word, case_sensitive, + files_to_include, + files_to_exclude, } } - pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { + pub fn regex( + query: impl ToString, + whole_word: bool, + case_sensitive: bool, + files_to_include: Vec, + files_to_exclude: Vec, + ) -> Result { let mut query = query.to_string(); let initial_query = Arc::from(query.as_str()); if whole_word { @@ -64,17 +83,43 @@ impl SearchQuery { multiline, whole_word, case_sensitive, + files_to_include, + files_to_exclude, }) } pub fn from_proto(message: proto::SearchProject) -> Result { if message.regex { - Self::regex(message.query, message.whole_word, message.case_sensitive) + Self::regex( + message.query, + message.whole_word, + message.case_sensitive, + message + .files_to_include + .split(',') + .map(|glob_str| glob::Pattern::new(glob_str)) + .collect::>()?, + message + .files_to_exclude + .split(',') + .map(|glob_str| glob::Pattern::new(glob_str)) + .collect::>()?, + ) } else { Ok(Self::text( message.query, message.whole_word, message.case_sensitive, + message + .files_to_include + .split(',') + .map(|glob_str| glob::Pattern::new(glob_str)) + .collect::>()?, + message + .files_to_exclude + .split(',') + .map(|glob_str| glob::Pattern::new(glob_str)) + .collect::>()?, )) } } @@ -86,6 +131,16 @@ impl SearchQuery { regex: self.is_regex(), whole_word: self.whole_word(), case_sensitive: self.case_sensitive(), + files_to_include: self + .files_to_include() + .iter() + .map(ToString::to_string) + .join(","), + files_to_exclude: self + .files_to_exclude() + .iter() + .map(ToString::to_string) + .join(","), } } @@ -224,4 +279,25 @@ impl SearchQuery { pub fn is_regex(&self) -> bool { matches!(self, Self::Regex { .. }) } + + pub fn files_to_include(&self) -> &[glob::Pattern] { + match self { + Self::Text { + files_to_include, .. + } => files_to_include, + Self::Regex { + files_to_include, .. + } => files_to_include, + } + } + pub fn files_to_exclude(&self) -> &[glob::Pattern] { + match self { + Self::Text { + files_to_exclude, .. + } => files_to_exclude, + Self::Regex { + files_to_exclude, .. + } => files_to_exclude, + } + } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d3b381bc5c..220ef22fb7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -680,6 +680,8 @@ message SearchProject { bool regex = 3; bool whole_word = 4; bool case_sensitive = 5; + string files_to_include = 6; + string files_to_exclude = 7; } message SearchProjectResponse { diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index eec0dd0e22..ab3c35c1fe 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -27,6 +27,7 @@ serde.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true +glob.workspace = true [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 91284a545f..b0af51379d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -573,7 +573,13 @@ impl BufferSearchBar { active_searchable_item.clear_matches(cx); } else { let query = if self.regex { - match SearchQuery::regex(query, self.whole_word, self.case_sensitive) { + match SearchQuery::regex( + query, + self.whole_word, + self.case_sensitive, + Vec::new(), + Vec::new(), + ) { Ok(query) => query, Err(_) => { self.query_contains_error = true; @@ -582,7 +588,13 @@ impl BufferSearchBar { } } } else { - SearchQuery::text(query, self.whole_word, self.case_sensitive) + SearchQuery::text( + query, + self.whole_word, + self.case_sensitive, + Vec::new(), + Vec::new(), + ) }; let matches = active_searchable_item.find_matches(query, cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d68cad71d8..fa5ce31f05 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -86,6 +86,8 @@ pub struct ProjectSearchView { active_match_index: Option, search_id: usize, query_editor_was_focused: bool, + included_files_editor: ViewHandle, + excluded_files_editor: ViewHandle, } pub struct ProjectSearchBar { @@ -448,6 +450,32 @@ impl ProjectSearchView { }) .detach(); + let included_files_editor = cx.add_view(|cx| { + let editor = Editor::single_line( + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ); + editor + }); + // Subcribe to include_files_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&included_files_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + + let excluded_files_editor = cx.add_view(|cx| { + let editor = Editor::single_line( + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ); + editor + }); + // Subcribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&excluded_files_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + let mut this = ProjectSearchView { search_id: model.read(cx).search_id, model, @@ -459,6 +487,8 @@ impl ProjectSearchView { query_contains_error: false, active_match_index: None, query_editor_was_focused: false, + included_files_editor, + excluded_files_editor, }; this.model_changed(cx); this @@ -525,8 +555,31 @@ impl ProjectSearchView { fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { let text = self.query_editor.read(cx).text(cx); + let included_files = self + .included_files_editor + .read(cx) + .text(cx) + .split(',') + .map(|glob_str| glob::Pattern::new(glob_str)) + .collect::>() + // TODO kb validation + .unwrap_or_default(); + let excluded_files = self + .excluded_files_editor + .read(cx) + .text(cx) + .split(',') + .map(|glob_str| glob::Pattern::new(glob_str)) + .collect::>() + .unwrap_or_default(); if self.regex { - match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { + match SearchQuery::regex( + text, + self.whole_word, + self.case_sensitive, + included_files, + excluded_files, + ) { Ok(query) => Some(query), Err(_) => { self.query_contains_error = true; @@ -539,6 +592,8 @@ impl ProjectSearchView { text, self.whole_word, self.case_sensitive, + included_files, + excluded_files, )) } } @@ -869,6 +924,16 @@ impl View for ProjectSearchBar { } else { theme.search.editor.input.container }; + + let included_files_view = ChildView::new(&search.included_files_editor, cx) + .aligned() + .left() + .flex(1., true); + let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) + .aligned() + .left() + .flex(1., true); + Flex::row() .with_child( Flex::row() @@ -918,6 +983,31 @@ impl View for ProjectSearchBar { .with_style(theme.search.option_button_group) .aligned(), ) + .with_child( + // TODO kb better layout + Flex::row() + .with_child( + Label::new("Include files:", theme.search.match_index.text.clone()) + .contained() + .with_style(theme.search.match_index.container) + .aligned(), + ) + .with_child(included_files_view) + .with_child( + Label::new("Exclude files:", theme.search.match_index.text.clone()) + .contained() + .with_style(theme.search.match_index.container) + .aligned(), + ) + .with_child(excluded_files_view) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false), + ) .contained() .with_style(theme.search.container) .aligned()