Detailed changes
@@ -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",
@@ -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();
@@ -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 {
@@ -58,6 +58,7 @@ similar = "1.3"
smol.workspace = true
thiserror.workspace = true
toml = "0.5"
+itertools = "0.10"
[dev-dependencies]
ctor.workspace = true
@@ -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]),
@@ -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<str>,
whole_word: bool,
case_sensitive: bool,
+ files_to_include: Vec<glob::Pattern>,
+ files_to_exclude: Vec<glob::Pattern>,
},
Regex {
regex: Regex,
@@ -24,11 +27,19 @@ pub enum SearchQuery {
multiline: bool,
whole_word: bool,
case_sensitive: bool,
+ files_to_include: Vec<glob::Pattern>,
+ files_to_exclude: Vec<glob::Pattern>,
},
}
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<glob::Pattern>,
+ files_to_exclude: Vec<glob::Pattern>,
+ ) -> 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<Self> {
+ pub fn regex(
+ query: impl ToString,
+ whole_word: bool,
+ case_sensitive: bool,
+ files_to_include: Vec<glob::Pattern>,
+ files_to_exclude: Vec<glob::Pattern>,
+ ) -> Result<Self> {
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<Self> {
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::<Result<_, _>>()?,
+ message
+ .files_to_exclude
+ .split(',')
+ .map(|glob_str| glob::Pattern::new(glob_str))
+ .collect::<Result<_, _>>()?,
+ )
} 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::<Result<_, _>>()?,
+ message
+ .files_to_exclude
+ .split(',')
+ .map(|glob_str| glob::Pattern::new(glob_str))
+ .collect::<Result<_, _>>()?,
))
}
}
@@ -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,
+ }
+ }
}
@@ -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 {
@@ -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"] }
@@ -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);
@@ -86,6 +86,8 @@ pub struct ProjectSearchView {
active_match_index: Option<usize>,
search_id: usize,
query_editor_was_focused: bool,
+ included_files_editor: ViewHandle<Editor>,
+ excluded_files_editor: ViewHandle<Editor>,
}
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<Self>) -> Option<SearchQuery> {
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::<Result<_, _>>()
+ // 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::<Result<_, _>>()
+ .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()