mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-24 02:46:43 +00:00
Merge branch 'main' into panels
This commit is contained in:
commit
cdcb7c8084
16 changed files with 841 additions and 133 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",
|
||||||
|
|
|
@ -199,6 +199,18 @@
|
||||||
"shift-enter": "search::SelectPrevMatch"
|
"shift-enter": "search::SelectPrevMatch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "ProjectSearchBar > Editor",
|
||||||
|
"bindings": {
|
||||||
|
"escape": "project_search::ToggleFocus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "ProjectSearchView > Editor",
|
||||||
|
"bindings": {
|
||||||
|
"escape": "project_search::ToggleFocus"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Pane",
|
"context": "Pane",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -126,7 +126,6 @@ pub struct ContextMenu {
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
previously_focused_view_id: Option<usize>,
|
previously_focused_view_id: Option<usize>,
|
||||||
clicked: bool,
|
|
||||||
parent_view_id: usize,
|
parent_view_id: usize,
|
||||||
_actions_observation: Subscription,
|
_actions_observation: Subscription,
|
||||||
}
|
}
|
||||||
|
@ -187,7 +186,6 @@ impl ContextMenu {
|
||||||
selected_index: Default::default(),
|
selected_index: Default::default(),
|
||||||
visible: Default::default(),
|
visible: Default::default(),
|
||||||
previously_focused_view_id: Default::default(),
|
previously_focused_view_id: Default::default(),
|
||||||
clicked: false,
|
|
||||||
parent_view_id,
|
parent_view_id,
|
||||||
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
||||||
}
|
}
|
||||||
|
@ -203,18 +201,14 @@ impl ContextMenu {
|
||||||
.iter()
|
.iter()
|
||||||
.position(|item| item.action_id() == Some(action_id))
|
.position(|item| item.action_id() == Some(action_id))
|
||||||
{
|
{
|
||||||
if self.clicked {
|
self.selected_index = Some(ix);
|
||||||
self.cancel(&Default::default(), cx);
|
cx.notify();
|
||||||
} else {
|
cx.spawn(|this, mut cx| async move {
|
||||||
self.selected_index = Some(ix);
|
cx.background().timer(Duration::from_millis(50)).await;
|
||||||
cx.notify();
|
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
||||||
cx.spawn(|this, mut cx| async move {
|
anyhow::Ok(())
|
||||||
cx.background().timer(Duration::from_millis(50)).await;
|
})
|
||||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
.detach_and_log_err(cx);
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +248,6 @@ impl ContextMenu {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
self.visible = false;
|
self.visible = false;
|
||||||
self.selected_index.take();
|
self.selected_index.take();
|
||||||
self.clicked = false;
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,7 +447,7 @@ impl ContextMenu {
|
||||||
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
|
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||||
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
|
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||||
.on_click(MouseButton::Left, move |_, menu, cx| {
|
.on_click(MouseButton::Left, move |_, menu, cx| {
|
||||||
menu.clicked = true;
|
menu.cancel(&Default::default(), cx);
|
||||||
let window_id = cx.window_id();
|
let window_id = cx.window_id();
|
||||||
match &action {
|
match &action {
|
||||||
ContextMenuItemAction::Action(action) => {
|
ContextMenuItemAction::Action(action) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4208,14 +4208,19 @@ impl Project {
|
||||||
if matching_paths_tx.is_closed() {
|
if matching_paths_tx.is_closed() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let matches = if query
|
||||||
abs_path.clear();
|
.file_matches(Some(&entry.path))
|
||||||
abs_path.push(&snapshot.abs_path());
|
|
||||||
abs_path.push(&entry.path);
|
|
||||||
let matches = if let Some(file) =
|
|
||||||
fs.open_sync(&abs_path).await.log_err()
|
|
||||||
{
|
{
|
||||||
query.detect(file).unwrap_or(false)
|
abs_path.clear();
|
||||||
|
abs_path.push(&snapshot.abs_path());
|
||||||
|
abs_path.push(&entry.path);
|
||||||
|
if let Some(file) =
|
||||||
|
fs.open_sync(&abs_path).await.log_err()
|
||||||
|
{
|
||||||
|
query.detect(file).unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
@ -4299,15 +4304,21 @@ impl Project {
|
||||||
let mut buffers_rx = buffers_rx.clone();
|
let mut buffers_rx = buffers_rx.clone();
|
||||||
scope.spawn(async move {
|
scope.spawn(async move {
|
||||||
while let Some((buffer, snapshot)) = buffers_rx.next().await {
|
while let Some((buffer, snapshot)) = buffers_rx.next().await {
|
||||||
let buffer_matches = query
|
let buffer_matches = if query.file_matches(
|
||||||
.search(snapshot.as_rope())
|
snapshot.file().map(|file| file.path().as_ref()),
|
||||||
.await
|
) {
|
||||||
.iter()
|
query
|
||||||
.map(|range| {
|
.search(snapshot.as_rope())
|
||||||
snapshot.anchor_before(range.start)
|
.await
|
||||||
..snapshot.anchor_after(range.end)
|
.iter()
|
||||||
})
|
.map(|range| {
|
||||||
.collect::<Vec<_>>();
|
snapshot.anchor_before(range.start)
|
||||||
|
..snapshot.anchor_after(range.end)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
if !buffer_matches.is_empty() {
|
if !buffer_matches.is_empty() {
|
||||||
worker_matched_buffers
|
worker_matched_buffers
|
||||||
.insert(buffer.clone(), buffer_matches);
|
.insert(buffer.clone(), buffer_matches);
|
||||||
|
|
|
@ -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,37 +3322,361 @@ 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]),
|
||||||
("four.rs".to_string(), vec![25..28, 36..39])
|
("four.rs".to_string(), vec![25..28, 36..39])
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
}
|
||||||
async fn search(
|
|
||||||
project: &ModelHandle<Project>,
|
#[gpui::test]
|
||||||
query: SearchQuery,
|
async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
cx: &mut gpui::TestAppContext,
|
let search_query = "file";
|
||||||
) -> Result<HashMap<String, Vec<Range<usize>>>> {
|
|
||||||
let results = project
|
let fs = FakeFs::new(cx.background());
|
||||||
.update(cx, |project, cx| project.search(query, cx))
|
fs.insert_tree(
|
||||||
.await?;
|
"/dir",
|
||||||
|
json!({
|
||||||
Ok(results
|
"one.rs": r#"// Rust file one"#,
|
||||||
.into_iter()
|
"one.ts": r#"// TypeScript file one"#,
|
||||||
.map(|(buffer, ranges)| {
|
"two.rs": r#"// Rust file two"#,
|
||||||
buffer.read_with(cx, |buffer, _| {
|
"two.ts": r#"// TypeScript file two"#,
|
||||||
let path = buffer.file().unwrap().path().to_string_lossy().to_string();
|
}),
|
||||||
let ranges = ranges
|
)
|
||||||
.into_iter()
|
.await;
|
||||||
.map(|range| range.to_offset(buffer))
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
.collect::<Vec<_>>();
|
|
||||||
(path, ranges)
|
assert!(
|
||||||
})
|
search(
|
||||||
})
|
&project,
|
||||||
.collect())
|
SearchQuery::text(
|
||||||
}
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||||
|
Vec::new()
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_empty(),
|
||||||
|
"If no inclusions match, no files should be returned"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![glob::Pattern::new("*.rs").unwrap()],
|
||||||
|
Vec::new()
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.rs".to_string(), vec![8..12]),
|
||||||
|
("two.rs".to_string(), vec![8..12]),
|
||||||
|
]),
|
||||||
|
"Rust only search should give only Rust files"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap(),
|
||||||
|
],
|
||||||
|
Vec::new()
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.ts".to_string(), vec![14..18]),
|
||||||
|
("two.ts".to_string(), vec![14..18]),
|
||||||
|
]),
|
||||||
|
"TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.rs").unwrap(),
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap(),
|
||||||
|
],
|
||||||
|
Vec::new()
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.rs".to_string(), vec![8..12]),
|
||||||
|
("one.ts".to_string(), vec![14..18]),
|
||||||
|
("two.rs".to_string(), vec![8..12]),
|
||||||
|
("two.ts".to_string(), vec![14..18]),
|
||||||
|
]),
|
||||||
|
"Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
|
let search_query = "file";
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"one.rs": r#"// Rust file one"#,
|
||||||
|
"one.ts": r#"// TypeScript file one"#,
|
||||||
|
"two.rs": r#"// Rust file two"#,
|
||||||
|
"two.ts": r#"// TypeScript file two"#,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Vec::new(),
|
||||||
|
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.rs".to_string(), vec![8..12]),
|
||||||
|
("one.ts".to_string(), vec![14..18]),
|
||||||
|
("two.rs".to_string(), vec![8..12]),
|
||||||
|
("two.ts".to_string(), vec![14..18]),
|
||||||
|
]),
|
||||||
|
"If no exclusions match, all files should be returned"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Vec::new(),
|
||||||
|
vec![glob::Pattern::new("*.rs").unwrap()],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.ts".to_string(), vec![14..18]),
|
||||||
|
("two.ts".to_string(), vec![14..18]),
|
||||||
|
]),
|
||||||
|
"Rust exclusion search should give only TypeScript files"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Vec::new(),
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.rs".to_string(), vec![8..12]),
|
||||||
|
("two.rs".to_string(), vec![8..12]),
|
||||||
|
]),
|
||||||
|
"TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
Vec::new(),
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.rs").unwrap(),
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap().is_empty(),
|
||||||
|
"Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
|
let search_query = "file";
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
"one.rs": r#"// Rust file one"#,
|
||||||
|
"one.ts": r#"// TypeScript file one"#,
|
||||||
|
"two.rs": r#"// Rust file two"#,
|
||||||
|
"two.ts": r#"// TypeScript file two"#,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||||
|
vec![glob::Pattern::new("*.odd").unwrap()],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_empty(),
|
||||||
|
"If both no exclusions and inclusions match, exclusions should win and return nothing"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![glob::Pattern::new("*.ts").unwrap()],
|
||||||
|
vec![glob::Pattern::new("*.ts").unwrap()],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_empty(),
|
||||||
|
"If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap()
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_empty(),
|
||||||
|
"Non-matching inclusions and exclusions should not change that."
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
search(
|
||||||
|
&project,
|
||||||
|
SearchQuery::text(
|
||||||
|
search_query,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.ts").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap()
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
glob::Pattern::new("*.rs").unwrap(),
|
||||||
|
glob::Pattern::new("*.odd").unwrap()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
cx
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
HashMap::from_iter([
|
||||||
|
("one.ts".to_string(), vec![14..18]),
|
||||||
|
("two.ts".to_string(), vec![14..18]),
|
||||||
|
]),
|
||||||
|
"Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search(
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
|
query: SearchQuery,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) -> Result<HashMap<String, Vec<Range<usize>>>> {
|
||||||
|
let results = project
|
||||||
|
.update(cx, |project, cx| project.search(query, cx))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(buffer, ranges)| {
|
||||||
|
buffer.read_with(cx, |buffer, _| {
|
||||||
|
let path = buffer.file().unwrap().path().to_string_lossy().to_string();
|
||||||
|
let ranges = ranges
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| range.to_offset(buffer))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(path, ranges)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
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;
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader, Read},
|
io::{BufRead, BufReader, Read},
|
||||||
ops::Range,
|
ops::Range,
|
||||||
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum SearchQuery {
|
pub enum SearchQuery {
|
||||||
Text {
|
Text {
|
||||||
search: Arc<AhoCorasick<usize>>,
|
search: Arc<AhoCorasick<usize>>,
|
||||||
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 +28,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 +51,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 +84,51 @@ 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(str::trim)
|
||||||
|
.filter(|glob_str| !glob_str.is_empty())
|
||||||
|
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
message
|
||||||
|
.files_to_exclude
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|glob_str| !glob_str.is_empty())
|
||||||
|
.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(str::trim)
|
||||||
|
.filter(|glob_str| !glob_str.is_empty())
|
||||||
|
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
message
|
||||||
|
.files_to_exclude
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|glob_str| !glob_str.is_empty())
|
||||||
|
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +140,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 +288,43 @@ 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
|
||||||
|
match file_path {
|
||||||
|
Some(file_path) => {
|
||||||
|
!self
|
||||||
|
.files_to_exclude()
|
||||||
|
.iter()
|
||||||
|
.any(|exclude_glob| exclude_glob.matches_path(file_path))
|
||||||
|
&& (self.files_to_include().is_empty()
|
||||||
|
|| self
|
||||||
|
.files_to_include()
|
||||||
|
.iter()
|
||||||
|
.any(|include_glob| include_glob.matches_path(file_path)))
|
||||||
|
}
|
||||||
|
None => self.files_to_include().is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -22,6 +22,7 @@ use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
collections::HashSet,
|
||||||
mem,
|
mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -34,7 +35,7 @@ use workspace::{
|
||||||
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(project_search, [SearchInNew, ToggleFocus]);
|
actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
|
struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
|
||||||
|
@ -48,6 +49,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(ProjectSearchBar::select_prev_match);
|
cx.add_action(ProjectSearchBar::select_prev_match);
|
||||||
cx.add_action(ProjectSearchBar::toggle_focus);
|
cx.add_action(ProjectSearchBar::toggle_focus);
|
||||||
cx.capture_action(ProjectSearchBar::tab);
|
cx.capture_action(ProjectSearchBar::tab);
|
||||||
|
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||||
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
||||||
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
|
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
|
||||||
|
@ -75,6 +77,13 @@ struct ProjectSearch {
|
||||||
search_id: usize,
|
search_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
enum InputPanel {
|
||||||
|
Query,
|
||||||
|
Exclude,
|
||||||
|
Include,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ProjectSearchView {
|
pub struct ProjectSearchView {
|
||||||
model: ModelHandle<ProjectSearch>,
|
model: ModelHandle<ProjectSearch>,
|
||||||
query_editor: ViewHandle<Editor>,
|
query_editor: ViewHandle<Editor>,
|
||||||
|
@ -82,10 +91,12 @@ pub struct ProjectSearchView {
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
regex: bool,
|
regex: bool,
|
||||||
query_contains_error: bool,
|
panels_with_errors: HashSet<InputPanel>,
|
||||||
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 {
|
||||||
|
@ -425,7 +436,7 @@ impl ProjectSearchView {
|
||||||
editor.set_text(query_text, cx);
|
editor.set_text(query_text, cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
// Subcribe to query_editor in order to reraise editor events for workspace item activation purposes
|
// Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
|
||||||
cx.subscribe(&query_editor, |_, _, event, cx| {
|
cx.subscribe(&query_editor, |_, _, event, cx| {
|
||||||
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
cx.emit(ViewEvent::EditorEvent(event.clone()))
|
||||||
})
|
})
|
||||||
|
@ -448,6 +459,40 @@ impl ProjectSearchView {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let included_files_editor = cx.add_view(|cx| {
|
||||||
|
let mut editor = Editor::single_line(
|
||||||
|
Some(Arc::new(|theme| {
|
||||||
|
theme.search.include_exclude_editor.input.clone()
|
||||||
|
})),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.set_placeholder_text("Include: crates/**/*.toml", cx);
|
||||||
|
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
// Subscribe 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 mut editor = Editor::single_line(
|
||||||
|
Some(Arc::new(|theme| {
|
||||||
|
theme.search.include_exclude_editor.input.clone()
|
||||||
|
})),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
|
||||||
|
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
// Subscribe 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,
|
||||||
|
@ -456,9 +501,11 @@ impl ProjectSearchView {
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
whole_word,
|
whole_word,
|
||||||
regex,
|
regex,
|
||||||
query_contains_error: false,
|
panels_with_errors: HashSet::new(),
|
||||||
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,11 +572,60 @@ 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 = match self
|
||||||
|
.included_files_editor
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|glob_str| !glob_str.is_empty())
|
||||||
|
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
{
|
||||||
|
Ok(included_files) => {
|
||||||
|
self.panels_with_errors.remove(&InputPanel::Include);
|
||||||
|
included_files
|
||||||
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
self.panels_with_errors.insert(InputPanel::Include);
|
||||||
|
cx.notify();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let excluded_files = match self
|
||||||
|
.excluded_files_editor
|
||||||
|
.read(cx)
|
||||||
|
.text(cx)
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|glob_str| !glob_str.is_empty())
|
||||||
|
.map(|glob_str| glob::Pattern::new(glob_str))
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
{
|
||||||
|
Ok(excluded_files) => {
|
||||||
|
self.panels_with_errors.remove(&InputPanel::Exclude);
|
||||||
|
excluded_files
|
||||||
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
self.panels_with_errors.insert(InputPanel::Exclude);
|
||||||
|
cx.notify();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
if self.regex {
|
if self.regex {
|
||||||
match SearchQuery::regex(text, self.whole_word, self.case_sensitive) {
|
match SearchQuery::regex(
|
||||||
Ok(query) => Some(query),
|
text,
|
||||||
Err(_) => {
|
self.whole_word,
|
||||||
self.query_contains_error = true;
|
self.case_sensitive,
|
||||||
|
included_files,
|
||||||
|
excluded_files,
|
||||||
|
) {
|
||||||
|
Ok(query) => {
|
||||||
|
self.panels_with_errors.remove(&InputPanel::Query);
|
||||||
|
Some(query)
|
||||||
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
self.panels_with_errors.insert(InputPanel::Query);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -539,6 +635,8 @@ impl ProjectSearchView {
|
||||||
text,
|
text,
|
||||||
self.whole_word,
|
self.whole_word,
|
||||||
self.case_sensitive,
|
self.case_sensitive,
|
||||||
|
included_files,
|
||||||
|
excluded_files,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -723,19 +821,50 @@ impl ProjectSearchBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
self.cycle_field(Direction::Next, cx);
|
||||||
search_view.update(cx, |search_view, cx| {
|
}
|
||||||
if search_view.query_editor.is_focused(cx) {
|
|
||||||
if !search_view.model.read(cx).match_ranges.is_empty() {
|
fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
|
||||||
search_view.focus_results_editor(cx);
|
self.cycle_field(Direction::Prev, cx);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||||
|
let active_project_search = match &self.active_project_search {
|
||||||
|
Some(active_project_search) => active_project_search,
|
||||||
|
|
||||||
|
None => {
|
||||||
|
cx.propagate_action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
active_project_search.update(cx, |project_view, cx| {
|
||||||
|
let views = &[
|
||||||
|
&project_view.query_editor,
|
||||||
|
&project_view.included_files_editor,
|
||||||
|
&project_view.excluded_files_editor,
|
||||||
|
];
|
||||||
|
|
||||||
|
let current_index = match views
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, view)| view.is_focused(cx))
|
||||||
|
{
|
||||||
|
Some((index, _)) => index,
|
||||||
|
|
||||||
|
None => {
|
||||||
cx.propagate_action();
|
cx.propagate_action();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
} else {
|
|
||||||
cx.propagate_action();
|
let new_index = match direction {
|
||||||
}
|
Direction::Next => (current_index + 1) % views.len(),
|
||||||
|
Direction::Prev if current_index == 0 => views.len() - 1,
|
||||||
|
Direction::Prev => (current_index - 1) % views.len(),
|
||||||
|
};
|
||||||
|
cx.focus(views[new_index]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
|
fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
@ -864,59 +993,121 @@ impl View for ProjectSearchBar {
|
||||||
if let Some(search) = self.active_project_search.as_ref() {
|
if let Some(search) = self.active_project_search.as_ref() {
|
||||||
let search = search.read(cx);
|
let search = search.read(cx);
|
||||||
let theme = cx.global::<Settings>().theme.clone();
|
let theme = cx.global::<Settings>().theme.clone();
|
||||||
let editor_container = if search.query_contains_error {
|
let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
|
||||||
theme.search.invalid_editor
|
theme.search.invalid_editor
|
||||||
} else {
|
} else {
|
||||||
theme.search.editor.input.container
|
theme.search.editor.input.container
|
||||||
};
|
};
|
||||||
Flex::row()
|
let include_container_style =
|
||||||
|
if search.panels_with_errors.contains(&InputPanel::Include) {
|
||||||
|
theme.search.invalid_include_exclude_editor
|
||||||
|
} else {
|
||||||
|
theme.search.include_exclude_editor.input.container
|
||||||
|
};
|
||||||
|
let exclude_container_style =
|
||||||
|
if search.panels_with_errors.contains(&InputPanel::Exclude) {
|
||||||
|
theme.search.invalid_include_exclude_editor
|
||||||
|
} else {
|
||||||
|
theme.search.include_exclude_editor.input.container
|
||||||
|
};
|
||||||
|
|
||||||
|
let included_files_view = ChildView::new(&search.included_files_editor, cx)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.flex(1.0, true);
|
||||||
|
let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx)
|
||||||
|
.aligned()
|
||||||
|
.right()
|
||||||
|
.flex(1.0, true);
|
||||||
|
|
||||||
|
let row_spacing = theme.workspace.toolbar.container.padding.bottom;
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
ChildView::new(&search.query_editor, cx)
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
ChildView::new(&search.query_editor, cx)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.with_children(search.active_match_index.map(|match_ix| {
|
||||||
|
Label::new(
|
||||||
|
format!(
|
||||||
|
"{}/{}",
|
||||||
|
match_ix + 1,
|
||||||
|
search.model.read(cx).match_ranges.len()
|
||||||
|
),
|
||||||
|
theme.search.match_index.text.clone(),
|
||||||
|
)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.search.match_index.container)
|
||||||
|
.aligned()
|
||||||
|
}))
|
||||||
|
.contained()
|
||||||
|
.with_style(query_container_style)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.constrained()
|
||||||
.flex(1., true),
|
.with_min_width(theme.search.editor.min_width)
|
||||||
|
.with_max_width(theme.search.editor.max_width)
|
||||||
|
.flex(1., false),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
||||||
|
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
||||||
|
.aligned(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_child(self.render_option_button(
|
||||||
|
"Case",
|
||||||
|
SearchOption::CaseSensitive,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.with_child(self.render_option_button(
|
||||||
|
"Word",
|
||||||
|
SearchOption::WholeWord,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.with_child(self.render_option_button(
|
||||||
|
"Regex",
|
||||||
|
SearchOption::Regex,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.search.option_button_group)
|
||||||
|
.aligned(),
|
||||||
)
|
)
|
||||||
.with_children(search.active_match_index.map(|match_ix| {
|
|
||||||
Label::new(
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
match_ix + 1,
|
|
||||||
search.model.read(cx).match_ranges.len()
|
|
||||||
),
|
|
||||||
theme.search.match_index.text.clone(),
|
|
||||||
)
|
|
||||||
.contained()
|
|
||||||
.with_style(theme.search.match_index.container)
|
|
||||||
.aligned()
|
|
||||||
}))
|
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(editor_container)
|
.with_margin_bottom(row_spacing),
|
||||||
.aligned()
|
|
||||||
.constrained()
|
|
||||||
.with_min_width(theme.search.editor.min_width)
|
|
||||||
.with_max_width(theme.search.editor.max_width)
|
|
||||||
.flex(1., false),
|
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(self.render_nav_button("<", Direction::Prev, cx))
|
.with_child(
|
||||||
.with_child(self.render_nav_button(">", Direction::Next, cx))
|
Flex::row()
|
||||||
.aligned(),
|
.with_child(included_files_view)
|
||||||
)
|
.contained()
|
||||||
.with_child(
|
.with_style(include_container_style)
|
||||||
Flex::row()
|
.aligned()
|
||||||
.with_child(self.render_option_button(
|
.constrained()
|
||||||
"Case",
|
.with_min_width(theme.search.include_exclude_editor.min_width)
|
||||||
SearchOption::CaseSensitive,
|
.with_max_width(theme.search.include_exclude_editor.max_width)
|
||||||
cx,
|
.flex(1., false),
|
||||||
))
|
)
|
||||||
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
|
.with_child(
|
||||||
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
|
Flex::row()
|
||||||
.contained()
|
.with_child(excluded_files_view)
|
||||||
.with_style(theme.search.option_button_group)
|
.contained()
|
||||||
.aligned(),
|
.with_style(exclude_container_style)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_min_width(theme.search.include_exclude_editor.min_width)
|
||||||
|
.with_max_width(theme.search.include_exclude_editor.max_width)
|
||||||
|
.flex(1., false),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.search.container)
|
.with_style(theme.search.container)
|
||||||
|
@ -948,6 +1139,10 @@ impl ToolbarItemView for ProjectSearchBar {
|
||||||
ToolbarItemLocation::Hidden
|
ToolbarItemLocation::Hidden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn row_count(&self) -> usize {
|
||||||
|
2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -309,6 +309,9 @@ pub struct Search {
|
||||||
pub editor: FindEditor,
|
pub editor: FindEditor,
|
||||||
pub invalid_editor: ContainerStyle,
|
pub invalid_editor: ContainerStyle,
|
||||||
pub option_button_group: ContainerStyle,
|
pub option_button_group: ContainerStyle,
|
||||||
|
pub include_exclude_editor: FindEditor,
|
||||||
|
pub invalid_include_exclude_editor: ContainerStyle,
|
||||||
|
pub include_exclude_inputs: ContainedText,
|
||||||
pub option_button: Interactive<ContainedText>,
|
pub option_button: Interactive<ContainedText>,
|
||||||
pub match_background: Color,
|
pub match_background: Color,
|
||||||
pub match_index: ContainedText,
|
pub match_index: ContainedText,
|
||||||
|
|
|
@ -22,6 +22,13 @@ pub trait ToolbarItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
|
fn pane_focus_update(&mut self, _pane_focused: bool, _cx: &mut ViewContext<Self>) {}
|
||||||
|
|
||||||
|
/// Number of times toolbar's height will be repeated to get the effective height.
|
||||||
|
/// Useful when multiple rows one under each other are needed.
|
||||||
|
/// The rows have the same width and act as a whole when reacting to resizes and similar events.
|
||||||
|
fn row_count(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ToolbarItemViewHandle {
|
trait ToolbarItemViewHandle {
|
||||||
|
@ -33,6 +40,7 @@ trait ToolbarItemViewHandle {
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> ToolbarItemLocation;
|
) -> ToolbarItemLocation;
|
||||||
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
|
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
|
||||||
|
fn row_count(&self, cx: &WindowContext) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
@ -66,12 +74,14 @@ impl View for Toolbar {
|
||||||
let mut primary_right_items = Vec::new();
|
let mut primary_right_items = Vec::new();
|
||||||
let mut secondary_item = None;
|
let mut secondary_item = None;
|
||||||
let spacing = theme.item_spacing;
|
let spacing = theme.item_spacing;
|
||||||
|
let mut primary_items_row_count = 1;
|
||||||
|
|
||||||
for (item, position) in &self.items {
|
for (item, position) in &self.items {
|
||||||
match *position {
|
match *position {
|
||||||
ToolbarItemLocation::Hidden => {}
|
ToolbarItemLocation::Hidden => {}
|
||||||
|
|
||||||
ToolbarItemLocation::PrimaryLeft { flex } => {
|
ToolbarItemLocation::PrimaryLeft { flex } => {
|
||||||
|
primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
|
||||||
let left_item = ChildView::new(item.as_any(), cx)
|
let left_item = ChildView::new(item.as_any(), cx)
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -84,6 +94,7 @@ impl View for Toolbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItemLocation::PrimaryRight { flex } => {
|
ToolbarItemLocation::PrimaryRight { flex } => {
|
||||||
|
primary_items_row_count = primary_items_row_count.max(item.row_count(cx));
|
||||||
let right_item = ChildView::new(item.as_any(), cx)
|
let right_item = ChildView::new(item.as_any(), cx)
|
||||||
.aligned()
|
.aligned()
|
||||||
.contained()
|
.contained()
|
||||||
|
@ -100,7 +111,7 @@ impl View for Toolbar {
|
||||||
secondary_item = Some(
|
secondary_item = Some(
|
||||||
ChildView::new(item.as_any(), cx)
|
ChildView::new(item.as_any(), cx)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_height(theme.height)
|
.with_height(theme.height * item.row_count(cx) as f32)
|
||||||
.into_any(),
|
.into_any(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -117,7 +128,8 @@ impl View for Toolbar {
|
||||||
}
|
}
|
||||||
|
|
||||||
let container_style = theme.container;
|
let container_style = theme.container;
|
||||||
let height = theme.height;
|
let height = theme.height * primary_items_row_count as f32;
|
||||||
|
let nav_button_height = theme.height;
|
||||||
let button_style = theme.nav_button;
|
let button_style = theme.nav_button;
|
||||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
|
|
||||||
|
@ -127,6 +139,7 @@ impl View for Toolbar {
|
||||||
.with_child(nav_button(
|
.with_child(nav_button(
|
||||||
"icons/arrow_left_16.svg",
|
"icons/arrow_left_16.svg",
|
||||||
button_style,
|
button_style,
|
||||||
|
nav_button_height,
|
||||||
tooltip_style.clone(),
|
tooltip_style.clone(),
|
||||||
enable_go_backward,
|
enable_go_backward,
|
||||||
spacing,
|
spacing,
|
||||||
|
@ -155,6 +168,7 @@ impl View for Toolbar {
|
||||||
.with_child(nav_button(
|
.with_child(nav_button(
|
||||||
"icons/arrow_right_16.svg",
|
"icons/arrow_right_16.svg",
|
||||||
button_style,
|
button_style,
|
||||||
|
nav_button_height,
|
||||||
tooltip_style,
|
tooltip_style,
|
||||||
enable_go_forward,
|
enable_go_forward,
|
||||||
spacing,
|
spacing,
|
||||||
|
@ -196,6 +210,7 @@ impl View for Toolbar {
|
||||||
fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
|
fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
|
||||||
svg_path: &'static str,
|
svg_path: &'static str,
|
||||||
style: theme::Interactive<theme::IconButton>,
|
style: theme::Interactive<theme::IconButton>,
|
||||||
|
nav_button_height: f32,
|
||||||
tooltip_style: TooltipStyle,
|
tooltip_style: TooltipStyle,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
spacing: f32,
|
spacing: f32,
|
||||||
|
@ -219,8 +234,9 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.constrained()
|
.constrained()
|
||||||
.with_width(style.button_width)
|
.with_width(style.button_width)
|
||||||
.with_height(style.button_width)
|
.with_height(nav_button_height)
|
||||||
.aligned()
|
.aligned()
|
||||||
|
.top()
|
||||||
})
|
})
|
||||||
.with_cursor_style(if enabled {
|
.with_cursor_style(if enabled {
|
||||||
CursorStyle::PointingHand
|
CursorStyle::PointingHand
|
||||||
|
@ -338,6 +354,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn row_count(&self, cx: &WindowContext) -> usize {
|
||||||
|
self.read(cx).row_count()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
|
impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
|
||||||
|
|
|
@ -26,6 +26,12 @@ export default function search(colorScheme: ColorScheme) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const includeExcludeEditor = {
|
||||||
|
...editor,
|
||||||
|
minWidth: 100,
|
||||||
|
maxWidth: 250,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
|
// TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
|
||||||
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
||||||
|
@ -64,9 +70,16 @@ export default function search(colorScheme: ColorScheme) {
|
||||||
...editor,
|
...editor,
|
||||||
border: border(layer, "negative"),
|
border: border(layer, "negative"),
|
||||||
},
|
},
|
||||||
|
includeExcludeEditor,
|
||||||
|
invalidIncludeExcludeEditor: {
|
||||||
|
...includeExcludeEditor,
|
||||||
|
border: border(layer, "negative"),
|
||||||
|
},
|
||||||
matchIndex: {
|
matchIndex: {
|
||||||
...text(layer, "mono", "variant"),
|
...text(layer, "mono", "variant"),
|
||||||
padding: 6,
|
padding: {
|
||||||
|
left: 6,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
optionButtonGroup: {
|
optionButtonGroup: {
|
||||||
padding: {
|
padding: {
|
||||||
|
@ -74,6 +87,12 @@ export default function search(colorScheme: ColorScheme) {
|
||||||
right: 12,
|
right: 12,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
includeExcludeInputs: {
|
||||||
|
...text(layer, "mono", "variant"),
|
||||||
|
padding: {
|
||||||
|
right: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
resultsStatus: {
|
resultsStatus: {
|
||||||
...text(layer, "mono", "on"),
|
...text(layer, "mono", "on"),
|
||||||
size: 18,
|
size: 18,
|
||||||
|
|
Loading…
Reference in a new issue