Add initial include/exclude project search UI
This commit is contained in:
parent
3115c8381d
commit
915154b047
10 changed files with 212 additions and 14 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4719,6 +4719,7 @@ dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"gpui",
|
"gpui",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"itertools",
|
||||||
"language",
|
"language",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -5771,6 +5772,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.25",
|
"futures 0.3.25",
|
||||||
|
"glob",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
@ -4548,7 +4548,10 @@ async fn test_project_search(
|
||||||
// Perform a search as the guest.
|
// Perform a search as the guest.
|
||||||
let results = project_b
|
let results = project_b
|
||||||
.update(cx_b, |project, cx| {
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -716,7 +716,10 @@ async fn apply_client_operation(
|
||||||
);
|
);
|
||||||
|
|
||||||
let search = project.update(cx, |project, cx| {
|
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);
|
drop(project);
|
||||||
let search = cx.background().spawn(async move {
|
let search = cx.background().spawn(async move {
|
||||||
|
|
|
@ -58,6 +58,7 @@ similar = "1.3"
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
itertools = "0.10"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
|
|
|
@ -3297,9 +3297,13 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||||
.await;
|
.await;
|
||||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search(&project, SearchQuery::text("TWO", false, true), cx)
|
search(
|
||||||
.await
|
&project,
|
||||||
.unwrap(),
|
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
HashMap::from_iter([
|
HashMap::from_iter([
|
||||||
("two.rs".to_string(), vec![6..9]),
|
("two.rs".to_string(), vec![6..9]),
|
||||||
("three.rs".to_string(), vec![37..40])
|
("three.rs".to_string(), vec![37..40])
|
||||||
|
@ -3318,9 +3322,13 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search(&project, SearchQuery::text("TWO", false, true), cx)
|
search(
|
||||||
.await
|
&project,
|
||||||
.unwrap(),
|
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
HashMap::from_iter([
|
HashMap::from_iter([
|
||||||
("two.rs".to_string(), vec![6..9]),
|
("two.rs".to_string(), vec![6..9]),
|
||||||
("three.rs".to_string(), vec![37..40]),
|
("three.rs".to_string(), vec![37..40]),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
|
use aho_corasick::{AhoCorasick, AhoCorasickBuilder};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::proto;
|
use client::proto;
|
||||||
|
use itertools::Itertools;
|
||||||
use language::{char_kind, Rope};
|
use language::{char_kind, Rope};
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
use smol::future::yield_now;
|
use smol::future::yield_now;
|
||||||
|
@ -17,6 +18,8 @@ pub enum SearchQuery {
|
||||||
query: Arc<str>,
|
query: Arc<str>,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
files_to_include: Vec<glob::Pattern>,
|
||||||
|
files_to_exclude: Vec<glob::Pattern>,
|
||||||
},
|
},
|
||||||
Regex {
|
Regex {
|
||||||
regex: Regex,
|
regex: Regex,
|
||||||
|
@ -24,11 +27,19 @@ pub enum SearchQuery {
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
files_to_include: Vec<glob::Pattern>,
|
||||||
|
files_to_exclude: Vec<glob::Pattern>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchQuery {
|
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 query = query.to_string();
|
||||||
let search = AhoCorasickBuilder::new()
|
let search = AhoCorasickBuilder::new()
|
||||||
.auto_configure(&[&query])
|
.auto_configure(&[&query])
|
||||||
|
@ -39,10 +50,18 @@ impl SearchQuery {
|
||||||
query: Arc::from(query),
|
query: Arc::from(query),
|
||||||
whole_word,
|
whole_word,
|
||||||
case_sensitive,
|
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 mut query = query.to_string();
|
||||||
let initial_query = Arc::from(query.as_str());
|
let initial_query = Arc::from(query.as_str());
|
||||||
if whole_word {
|
if whole_word {
|
||||||
|
@ -64,17 +83,43 @@ impl SearchQuery {
|
||||||
multiline,
|
multiline,
|
||||||
whole_word,
|
whole_word,
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
|
files_to_include,
|
||||||
|
files_to_exclude,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_proto(message: proto::SearchProject) -> Result<Self> {
|
pub fn from_proto(message: proto::SearchProject) -> Result<Self> {
|
||||||
if message.regex {
|
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 {
|
} else {
|
||||||
Ok(Self::text(
|
Ok(Self::text(
|
||||||
message.query,
|
message.query,
|
||||||
message.whole_word,
|
message.whole_word,
|
||||||
message.case_sensitive,
|
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(),
|
regex: self.is_regex(),
|
||||||
whole_word: self.whole_word(),
|
whole_word: self.whole_word(),
|
||||||
case_sensitive: self.case_sensitive(),
|
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 {
|
pub fn is_regex(&self) -> bool {
|
||||||
matches!(self, Self::Regex { .. })
|
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 regex = 3;
|
||||||
bool whole_word = 4;
|
bool whole_word = 4;
|
||||||
bool case_sensitive = 5;
|
bool case_sensitive = 5;
|
||||||
|
string files_to_include = 6;
|
||||||
|
string files_to_exclude = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SearchProjectResponse {
|
message SearchProjectResponse {
|
||||||
|
|
|
@ -27,6 +27,7 @@ serde.workspace = true
|
||||||
serde_derive.workspace = true
|
serde_derive.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
glob.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
|
|
@ -573,7 +573,13 @@ impl BufferSearchBar {
|
||||||
active_searchable_item.clear_matches(cx);
|
active_searchable_item.clear_matches(cx);
|
||||||
} else {
|
} else {
|
||||||
let query = if self.regex {
|
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,
|
Ok(query) => query,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.query_contains_error = true;
|
self.query_contains_error = true;
|
||||||
|
@ -582,7 +588,13 @@ impl BufferSearchBar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
let matches = active_searchable_item.find_matches(query, cx);
|
||||||
|
|
|
@ -86,6 +86,8 @@ pub struct ProjectSearchView {
|
||||||
active_match_index: Option<usize>,
|
active_match_index: Option<usize>,
|
||||||
search_id: usize,
|
search_id: usize,
|
||||||
query_editor_was_focused: bool,
|
query_editor_was_focused: bool,
|
||||||
|
included_files_editor: ViewHandle<Editor>,
|
||||||
|
excluded_files_editor: ViewHandle<Editor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ProjectSearchBar {
|
pub struct ProjectSearchBar {
|
||||||
|
@ -448,6 +450,32 @@ impl ProjectSearchView {
|
||||||
})
|
})
|
||||||
.detach();
|
.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 {
|
let mut this = ProjectSearchView {
|
||||||
search_id: model.read(cx).search_id,
|
search_id: model.read(cx).search_id,
|
||||||
model,
|
model,
|
||||||
|
@ -459,6 +487,8 @@ impl ProjectSearchView {
|
||||||
query_contains_error: false,
|
query_contains_error: false,
|
||||||
active_match_index: None,
|
active_match_index: None,
|
||||||
query_editor_was_focused: false,
|
query_editor_was_focused: false,
|
||||||
|
included_files_editor,
|
||||||
|
excluded_files_editor,
|
||||||
};
|
};
|
||||||
this.model_changed(cx);
|
this.model_changed(cx);
|
||||||
this
|
this
|
||||||
|
@ -525,8 +555,31 @@ impl ProjectSearchView {
|
||||||
|
|
||||||
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
|
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
|
||||||
let text = self.query_editor.read(cx).text(cx);
|
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 {
|
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),
|
Ok(query) => Some(query),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.query_contains_error = true;
|
self.query_contains_error = true;
|
||||||
|
@ -539,6 +592,8 @@ impl ProjectSearchView {
|
||||||
text,
|
text,
|
||||||
self.whole_word,
|
self.whole_word,
|
||||||
self.case_sensitive,
|
self.case_sensitive,
|
||||||
|
included_files,
|
||||||
|
excluded_files,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -869,6 +924,16 @@ impl View for ProjectSearchBar {
|
||||||
} else {
|
} else {
|
||||||
theme.search.editor.input.container
|
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()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
@ -918,6 +983,31 @@ impl View for ProjectSearchBar {
|
||||||
.with_style(theme.search.option_button_group)
|
.with_style(theme.search.option_button_group)
|
||||||
.aligned(),
|
.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()
|
.contained()
|
||||||
.with_style(theme.search.container)
|
.with_style(theme.search.container)
|
||||||
.aligned()
|
.aligned()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue