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",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"itertools",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -5771,6 +5772,7 @@ dependencies = [
|
|||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"glob",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
|
|
|
@ -199,6 +199,18 @@
|
|||
"shift-enter": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar > Editor",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView > Editor",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -126,7 +126,6 @@ pub struct ContextMenu {
|
|||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
clicked: bool,
|
||||
parent_view_id: usize,
|
||||
_actions_observation: Subscription,
|
||||
}
|
||||
|
@ -187,7 +186,6 @@ impl ContextMenu {
|
|||
selected_index: Default::default(),
|
||||
visible: Default::default(),
|
||||
previously_focused_view_id: Default::default(),
|
||||
clicked: false,
|
||||
parent_view_id,
|
||||
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
||||
}
|
||||
|
@ -203,18 +201,14 @@ impl ContextMenu {
|
|||
.iter()
|
||||
.position(|item| item.action_id() == Some(action_id))
|
||||
{
|
||||
if self.clicked {
|
||||
self.cancel(&Default::default(), cx);
|
||||
} else {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background().timer(Duration::from_millis(50)).await;
|
||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background().timer(Duration::from_millis(50)).await;
|
||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +248,6 @@ impl ContextMenu {
|
|||
self.items.clear();
|
||||
self.visible = false;
|
||||
self.selected_index.take();
|
||||
self.clicked = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -454,7 +447,7 @@ impl ContextMenu {
|
|||
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||
.on_click(MouseButton::Left, move |_, menu, cx| {
|
||||
menu.clicked = true;
|
||||
menu.cancel(&Default::default(), cx);
|
||||
let window_id = cx.window_id();
|
||||
match &action {
|
||||
ContextMenuItemAction::Action(action) => {
|
||||
|
|
|
@ -58,6 +58,7 @@ similar = "1.3"
|
|||
smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml = "0.5"
|
||||
itertools = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
|
|
@ -4208,14 +4208,19 @@ impl Project {
|
|||
if matching_paths_tx.is_closed() {
|
||||
break;
|
||||
}
|
||||
|
||||
abs_path.clear();
|
||||
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()
|
||||
let matches = if query
|
||||
.file_matches(Some(&entry.path))
|
||||
{
|
||||
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 {
|
||||
false
|
||||
};
|
||||
|
@ -4299,15 +4304,21 @@ impl Project {
|
|||
let mut buffers_rx = buffers_rx.clone();
|
||||
scope.spawn(async move {
|
||||
while let Some((buffer, snapshot)) = buffers_rx.next().await {
|
||||
let buffer_matches = query
|
||||
.search(snapshot.as_rope())
|
||||
.await
|
||||
.iter()
|
||||
.map(|range| {
|
||||
snapshot.anchor_before(range.start)
|
||||
..snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let buffer_matches = if query.file_matches(
|
||||
snapshot.file().map(|file| file.path().as_ref()),
|
||||
) {
|
||||
query
|
||||
.search(snapshot.as_rope())
|
||||
.await
|
||||
.iter()
|
||||
.map(|range| {
|
||||
snapshot.anchor_before(range.start)
|
||||
..snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if !buffer_matches.is_empty() {
|
||||
worker_matched_buffers
|
||||
.insert(buffer.clone(), buffer_matches);
|
||||
|
|
|
@ -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,37 +3322,361 @@ 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]),
|
||||
("four.rs".to_string(), vec![25..28, 36..39])
|
||||
])
|
||||
);
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_with_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::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 anyhow::Result;
|
||||
use client::proto;
|
||||
use itertools::Itertools;
|
||||
use language::{char_kind, Rope};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use smol::future::yield_now;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
ops::Range,
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SearchQuery {
|
||||
Text {
|
||||
search: Arc<AhoCorasick<usize>>,
|
||||
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 +28,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 +51,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 +84,51 @@ 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(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 {
|
||||
Ok(Self::text(
|
||||
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<_, _>>()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +140,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 +288,43 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
|
|
@ -22,6 +22,7 @@ use smallvec::SmallVec;
|
|||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
|
@ -34,7 +35,7 @@ use workspace::{
|
|||
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(project_search, [SearchInNew, ToggleFocus]);
|
||||
actions!(project_search, [SearchInNew, ToggleFocus, NextField]);
|
||||
|
||||
#[derive(Default)]
|
||||
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::toggle_focus);
|
||||
cx.capture_action(ProjectSearchBar::tab);
|
||||
cx.capture_action(ProjectSearchBar::tab_previous);
|
||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
|
||||
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
|
||||
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
|
||||
|
@ -75,6 +77,13 @@ struct ProjectSearch {
|
|||
search_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum InputPanel {
|
||||
Query,
|
||||
Exclude,
|
||||
Include,
|
||||
}
|
||||
|
||||
pub struct ProjectSearchView {
|
||||
model: ModelHandle<ProjectSearch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
|
@ -82,10 +91,12 @@ pub struct ProjectSearchView {
|
|||
case_sensitive: bool,
|
||||
whole_word: bool,
|
||||
regex: bool,
|
||||
query_contains_error: bool,
|
||||
panels_with_errors: HashSet<InputPanel>,
|
||||
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 {
|
||||
|
@ -425,7 +436,7 @@ impl ProjectSearchView {
|
|||
editor.set_text(query_text, cx);
|
||||
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.emit(ViewEvent::EditorEvent(event.clone()))
|
||||
})
|
||||
|
@ -448,6 +459,40 @@ impl ProjectSearchView {
|
|||
})
|
||||
.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 {
|
||||
search_id: model.read(cx).search_id,
|
||||
model,
|
||||
|
@ -456,9 +501,11 @@ impl ProjectSearchView {
|
|||
case_sensitive,
|
||||
whole_word,
|
||||
regex,
|
||||
query_contains_error: false,
|
||||
panels_with_errors: HashSet::new(),
|
||||
active_match_index: None,
|
||||
query_editor_was_focused: false,
|
||||
included_files_editor,
|
||||
excluded_files_editor,
|
||||
};
|
||||
this.model_changed(cx);
|
||||
this
|
||||
|
@ -525,11 +572,60 @@ 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 = 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 {
|
||||
match SearchQuery::regex(text, self.whole_word, self.case_sensitive) {
|
||||
Ok(query) => Some(query),
|
||||
Err(_) => {
|
||||
self.query_contains_error = true;
|
||||
match SearchQuery::regex(
|
||||
text,
|
||||
self.whole_word,
|
||||
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();
|
||||
None
|
||||
}
|
||||
|
@ -539,6 +635,8 @@ impl ProjectSearchView {
|
|||
text,
|
||||
self.whole_word,
|
||||
self.case_sensitive,
|
||||
included_files,
|
||||
excluded_files,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -723,19 +821,50 @@ impl ProjectSearchBar {
|
|||
}
|
||||
|
||||
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
|
||||
if let Some(search_view) = self.active_project_search.as_ref() {
|
||||
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() {
|
||||
search_view.focus_results_editor(cx);
|
||||
}
|
||||
} else {
|
||||
self.cycle_field(Direction::Next, cx);
|
||||
}
|
||||
|
||||
fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
|
||||
self.cycle_field(Direction::Prev, cx);
|
||||
}
|
||||
|
||||
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();
|
||||
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 {
|
||||
|
@ -864,59 +993,121 @@ impl View for ProjectSearchBar {
|
|||
if let Some(search) = self.active_project_search.as_ref() {
|
||||
let search = search.read(cx);
|
||||
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
|
||||
} else {
|
||||
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(
|
||||
Flex::row()
|
||||
.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()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
.constrained()
|
||||
.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()
|
||||
.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),
|
||||
.with_margin_bottom(row_spacing),
|
||||
)
|
||||
.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_child(
|
||||
Flex::row()
|
||||
.with_child(included_files_view)
|
||||
.contained()
|
||||
.with_style(include_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),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(excluded_files_view)
|
||||
.contained()
|
||||
.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()
|
||||
.with_style(theme.search.container)
|
||||
|
@ -948,6 +1139,10 @@ impl ToolbarItemView for ProjectSearchBar {
|
|||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -309,6 +309,9 @@ pub struct Search {
|
|||
pub editor: FindEditor,
|
||||
pub invalid_editor: 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 match_background: Color,
|
||||
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>) {}
|
||||
|
||||
/// 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 {
|
||||
|
@ -33,6 +40,7 @@ trait ToolbarItemViewHandle {
|
|||
cx: &mut WindowContext,
|
||||
) -> ToolbarItemLocation;
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
|
||||
fn row_count(&self, cx: &WindowContext) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
|
@ -66,12 +74,14 @@ impl View for Toolbar {
|
|||
let mut primary_right_items = Vec::new();
|
||||
let mut secondary_item = None;
|
||||
let spacing = theme.item_spacing;
|
||||
let mut primary_items_row_count = 1;
|
||||
|
||||
for (item, position) in &self.items {
|
||||
match *position {
|
||||
ToolbarItemLocation::Hidden => {}
|
||||
|
||||
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)
|
||||
.aligned()
|
||||
.contained()
|
||||
|
@ -84,6 +94,7 @@ impl View for Toolbar {
|
|||
}
|
||||
|
||||
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)
|
||||
.aligned()
|
||||
.contained()
|
||||
|
@ -100,7 +111,7 @@ impl View for Toolbar {
|
|||
secondary_item = Some(
|
||||
ChildView::new(item.as_any(), cx)
|
||||
.constrained()
|
||||
.with_height(theme.height)
|
||||
.with_height(theme.height * item.row_count(cx) as f32)
|
||||
.into_any(),
|
||||
);
|
||||
}
|
||||
|
@ -117,7 +128,8 @@ impl View for Toolbar {
|
|||
}
|
||||
|
||||
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 tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
|
||||
|
@ -127,6 +139,7 @@ impl View for Toolbar {
|
|||
.with_child(nav_button(
|
||||
"icons/arrow_left_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style.clone(),
|
||||
enable_go_backward,
|
||||
spacing,
|
||||
|
@ -155,6 +168,7 @@ impl View for Toolbar {
|
|||
.with_child(nav_button(
|
||||
"icons/arrow_right_16.svg",
|
||||
button_style,
|
||||
nav_button_height,
|
||||
tooltip_style,
|
||||
enable_go_forward,
|
||||
spacing,
|
||||
|
@ -196,6 +210,7 @@ impl View for Toolbar {
|
|||
fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
|
||||
svg_path: &'static str,
|
||||
style: theme::Interactive<theme::IconButton>,
|
||||
nav_button_height: f32,
|
||||
tooltip_style: TooltipStyle,
|
||||
enabled: bool,
|
||||
spacing: f32,
|
||||
|
@ -219,8 +234,9 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
|
|||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.with_height(nav_button_height)
|
||||
.aligned()
|
||||
.top()
|
||||
})
|
||||
.with_cursor_style(if enabled {
|
||||
CursorStyle::PointingHand
|
||||
|
@ -338,6 +354,10 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
|
|||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn row_count(&self, cx: &WindowContext) -> usize {
|
||||
self.read(cx).row_count()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle {
|
||||
|
|
|
@ -26,6 +26,12 @@ export default function search(colorScheme: ColorScheme) {
|
|||
},
|
||||
}
|
||||
|
||||
const includeExcludeEditor = {
|
||||
...editor,
|
||||
minWidth: 100,
|
||||
maxWidth: 250,
|
||||
};
|
||||
|
||||
return {
|
||||
// TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
|
||||
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
||||
|
@ -64,9 +70,16 @@ export default function search(colorScheme: ColorScheme) {
|
|||
...editor,
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
includeExcludeEditor,
|
||||
invalidIncludeExcludeEditor: {
|
||||
...includeExcludeEditor,
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
matchIndex: {
|
||||
...text(layer, "mono", "variant"),
|
||||
padding: 6,
|
||||
padding: {
|
||||
left: 6,
|
||||
},
|
||||
},
|
||||
optionButtonGroup: {
|
||||
padding: {
|
||||
|
@ -74,6 +87,12 @@ export default function search(colorScheme: ColorScheme) {
|
|||
right: 12,
|
||||
},
|
||||
},
|
||||
includeExcludeInputs: {
|
||||
...text(layer, "mono", "variant"),
|
||||
padding: {
|
||||
right: 6,
|
||||
},
|
||||
},
|
||||
resultsStatus: {
|
||||
...text(layer, "mono", "on"),
|
||||
size: 18,
|
||||
|
|
Loading…
Reference in a new issue