2023-06-28 03:46:08 +00:00
|
|
|
use bitflags::bitflags;
|
2022-03-29 15:04:39 +00:00
|
|
|
pub use buffer_search::BufferSearchBar;
|
2023-04-06 21:49:03 +00:00
|
|
|
use gpui::{actions, Action, AppContext};
|
2023-06-28 03:46:08 +00:00
|
|
|
use project::search::SearchQuery;
|
2022-03-31 16:36:39 +00:00
|
|
|
pub use project_search::{ProjectSearchBar, ProjectSearchView};
|
2023-07-31 13:40:03 +00:00
|
|
|
use smallvec::SmallVec;
|
2022-02-28 09:34:11 +00:00
|
|
|
|
2022-03-29 15:04:39 +00:00
|
|
|
pub mod buffer_search;
|
|
|
|
pub mod project_search;
|
2022-01-28 22:00:00 +00:00
|
|
|
|
2023-04-06 21:49:03 +00:00
|
|
|
pub fn init(cx: &mut AppContext) {
|
2022-02-27 15:15:38 +00:00
|
|
|
buffer_search::init(cx);
|
|
|
|
project_search::init(cx);
|
2022-01-28 22:00:00 +00:00
|
|
|
}
|
2022-01-28 00:16:51 +00:00
|
|
|
|
2022-06-16 12:28:37 +00:00
|
|
|
actions!(
|
|
|
|
search,
|
|
|
|
[
|
|
|
|
ToggleWholeWord,
|
|
|
|
ToggleCaseSensitive,
|
|
|
|
ToggleRegex,
|
|
|
|
SelectNextMatch,
|
2023-07-13 11:29:02 +00:00
|
|
|
SelectPrevMatch,
|
2023-07-13 12:39:00 +00:00
|
|
|
SelectAllMatches,
|
2023-07-31 13:40:03 +00:00
|
|
|
NextHistoryQuery,
|
|
|
|
PreviousHistoryQuery,
|
2022-06-16 12:28:37 +00:00
|
|
|
]
|
|
|
|
);
|
2022-02-27 21:18:04 +00:00
|
|
|
|
2023-06-28 03:46:08 +00:00
|
|
|
bitflags! {
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct SearchOptions: u8 {
|
|
|
|
const NONE = 0b000;
|
|
|
|
const WHOLE_WORD = 0b001;
|
|
|
|
const CASE_SENSITIVE = 0b010;
|
|
|
|
const REGEX = 0b100;
|
|
|
|
}
|
2022-01-28 00:16:51 +00:00
|
|
|
}
|
2022-02-27 21:18:04 +00:00
|
|
|
|
2023-06-28 03:46:08 +00:00
|
|
|
impl SearchOptions {
|
2022-06-16 11:44:00 +00:00
|
|
|
pub fn label(&self) -> &'static str {
|
2023-06-28 03:46:08 +00:00
|
|
|
match *self {
|
|
|
|
SearchOptions::WHOLE_WORD => "Match Whole Word",
|
|
|
|
SearchOptions::CASE_SENSITIVE => "Match Case",
|
|
|
|
SearchOptions::REGEX => "Use Regular Expression",
|
|
|
|
_ => panic!("{:?} is not a named SearchOption", self),
|
2022-06-16 11:44:00 +00:00
|
|
|
}
|
|
|
|
}
|
2022-06-16 12:28:37 +00:00
|
|
|
|
|
|
|
pub fn to_toggle_action(&self) -> Box<dyn Action> {
|
2023-06-28 03:46:08 +00:00
|
|
|
match *self {
|
|
|
|
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
|
|
|
|
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
|
|
|
SearchOptions::REGEX => Box::new(ToggleRegex),
|
|
|
|
_ => panic!("{:?} is not a named SearchOption", self),
|
2022-06-16 12:28:37 +00:00
|
|
|
}
|
|
|
|
}
|
2023-06-28 03:46:08 +00:00
|
|
|
|
2023-07-26 13:50:38 +00:00
|
|
|
pub fn none() -> SearchOptions {
|
|
|
|
SearchOptions::NONE
|
|
|
|
}
|
|
|
|
|
2023-06-28 03:46:08 +00:00
|
|
|
pub fn from_query(query: &SearchQuery) -> SearchOptions {
|
|
|
|
let mut options = SearchOptions::NONE;
|
|
|
|
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
|
|
|
|
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
|
|
|
|
options.set(SearchOptions::REGEX, query.is_regex());
|
|
|
|
options
|
|
|
|
}
|
2022-06-16 11:44:00 +00:00
|
|
|
}
|
2023-07-31 13:40:03 +00:00
|
|
|
|
|
|
|
const SEARCH_HISTORY_LIMIT: usize = 20;
|
|
|
|
|
|
|
|
#[derive(Default, Debug)]
|
|
|
|
pub struct SearchHistory {
|
|
|
|
history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
|
|
|
|
selected: Option<usize>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SearchHistory {
|
|
|
|
pub fn add(&mut self, search_string: String) {
|
|
|
|
if let Some(i) = self.selected {
|
|
|
|
if search_string == self.history[i] {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(previously_searched) = self.history.last_mut() {
|
|
|
|
if search_string.find(previously_searched.as_str()).is_some() {
|
|
|
|
*previously_searched = search_string;
|
|
|
|
self.selected = Some(self.history.len() - 1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
self.history.push(search_string);
|
|
|
|
if self.history.len() > SEARCH_HISTORY_LIMIT {
|
|
|
|
self.history.remove(0);
|
|
|
|
}
|
|
|
|
self.selected = Some(self.history.len() - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn next(&mut self) -> Option<&str> {
|
|
|
|
let history_size = self.history.len();
|
|
|
|
if history_size == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let selected = self.selected?;
|
|
|
|
if selected == history_size - 1 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let next_index = selected + 1;
|
|
|
|
self.selected = Some(next_index);
|
|
|
|
Some(&self.history[next_index])
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn current(&self) -> Option<&str> {
|
|
|
|
Some(&self.history[self.selected?])
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn previous(&mut self) -> Option<&str> {
|
|
|
|
let history_size = self.history.len();
|
|
|
|
if history_size == 0 {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let prev_index = match self.selected {
|
|
|
|
Some(selected_index) => {
|
|
|
|
if selected_index == 0 {
|
|
|
|
return None;
|
|
|
|
} else {
|
|
|
|
selected_index - 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => history_size - 1,
|
|
|
|
};
|
|
|
|
|
|
|
|
self.selected = Some(prev_index);
|
|
|
|
Some(&self.history[prev_index])
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn reset_selection(&mut self) {
|
|
|
|
self.selected = None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_add() {
|
|
|
|
let mut search_history = SearchHistory::default();
|
|
|
|
assert_eq!(
|
|
|
|
search_history.current(),
|
|
|
|
None,
|
|
|
|
"No current selection should be set fo the default search history"
|
|
|
|
);
|
|
|
|
|
|
|
|
search_history.add("rust".to_string());
|
|
|
|
assert_eq!(
|
|
|
|
search_history.current(),
|
|
|
|
Some("rust"),
|
|
|
|
"Newly added item should be selected"
|
|
|
|
);
|
|
|
|
|
|
|
|
// check if duplicates are not added
|
|
|
|
search_history.add("rust".to_string());
|
|
|
|
assert_eq!(
|
|
|
|
search_history.history.len(),
|
|
|
|
1,
|
|
|
|
"Should not add a duplicate"
|
|
|
|
);
|
|
|
|
assert_eq!(search_history.current(), Some("rust"));
|
|
|
|
|
|
|
|
// check if new string containing the previous string replaces it
|
|
|
|
search_history.add("rustlang".to_string());
|
|
|
|
assert_eq!(
|
|
|
|
search_history.history.len(),
|
|
|
|
1,
|
|
|
|
"Should replace previous item if it's a substring"
|
|
|
|
);
|
|
|
|
assert_eq!(search_history.current(), Some("rustlang"));
|
|
|
|
|
|
|
|
// push enough items to test SEARCH_HISTORY_LIMIT
|
|
|
|
for i in 0..SEARCH_HISTORY_LIMIT * 2 {
|
|
|
|
search_history.add(format!("item{i}"));
|
|
|
|
}
|
|
|
|
assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_next_and_previous() {
|
|
|
|
let mut search_history = SearchHistory::default();
|
|
|
|
assert_eq!(
|
|
|
|
search_history.next(),
|
|
|
|
None,
|
|
|
|
"Default search history should not have a next item"
|
|
|
|
);
|
|
|
|
|
|
|
|
search_history.add("Rust".to_string());
|
|
|
|
assert_eq!(search_history.next(), None);
|
|
|
|
search_history.add("JavaScript".to_string());
|
|
|
|
assert_eq!(search_history.next(), None);
|
|
|
|
search_history.add("TypeScript".to_string());
|
|
|
|
assert_eq!(search_history.next(), None);
|
|
|
|
|
|
|
|
assert_eq!(search_history.current(), Some("TypeScript"));
|
|
|
|
|
|
|
|
assert_eq!(search_history.previous(), Some("JavaScript"));
|
|
|
|
assert_eq!(search_history.current(), Some("JavaScript"));
|
|
|
|
|
|
|
|
assert_eq!(search_history.previous(), Some("Rust"));
|
|
|
|
assert_eq!(search_history.current(), Some("Rust"));
|
|
|
|
|
|
|
|
assert_eq!(search_history.previous(), None);
|
|
|
|
assert_eq!(search_history.current(), Some("Rust"));
|
|
|
|
|
|
|
|
assert_eq!(search_history.next(), Some("JavaScript"));
|
|
|
|
assert_eq!(search_history.current(), Some("JavaScript"));
|
|
|
|
|
|
|
|
assert_eq!(search_history.next(), Some("TypeScript"));
|
|
|
|
assert_eq!(search_history.current(), Some("TypeScript"));
|
|
|
|
|
|
|
|
assert_eq!(search_history.next(), None);
|
|
|
|
assert_eq!(search_history.current(), Some("TypeScript"));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_reset_selection() {
|
|
|
|
let mut search_history = SearchHistory::default();
|
|
|
|
search_history.add("Rust".to_string());
|
|
|
|
search_history.add("JavaScript".to_string());
|
|
|
|
search_history.add("TypeScript".to_string());
|
|
|
|
|
|
|
|
assert_eq!(search_history.current(), Some("TypeScript"));
|
|
|
|
search_history.reset_selection();
|
|
|
|
assert_eq!(search_history.current(), None);
|
|
|
|
assert_eq!(
|
|
|
|
search_history.previous(),
|
|
|
|
Some("TypeScript"),
|
|
|
|
"Should start from the end after reset on previous item query"
|
|
|
|
);
|
|
|
|
|
|
|
|
search_history.previous();
|
|
|
|
assert_eq!(search_history.current(), Some("JavaScript"));
|
|
|
|
search_history.previous();
|
|
|
|
assert_eq!(search_history.current(), Some("Rust"));
|
|
|
|
|
|
|
|
search_history.reset_selection();
|
|
|
|
assert_eq!(search_history.current(), None);
|
|
|
|
}
|
|
|
|
}
|