use bevy::prelude::Resource;
use super::{
error::VimError,
motion::{clamp_to_boundary, clamp_to_cursor_position},
};
const MAX_SEARCH_QUERY_CHARS: usize = 256;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchDirection {
Forward,
Backward,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchRepeatDirection {
Next,
Previous,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SearchQuery {
text: String,
}
impl SearchQuery {
#[must_use]
pub const fn new() -> Self {
Self {
text: String::new(),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.text
}
#[must_use]
pub fn case_sensitivity(&self) -> SearchCaseSensitivity {
if self.text.chars().any(char::is_uppercase) {
SearchCaseSensitivity::Sensitive
} else {
SearchCaseSensitivity::Insensitive
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
pub fn push_str(&mut self, text: &str) {
let remaining = MAX_SEARCH_QUERY_CHARS.saturating_sub(self.text.chars().count());
self.text.extend(text.chars().take(remaining));
}
pub fn pop_char(&mut self) {
let _removed = self.text.pop();
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchCaseSensitivity {
Sensitive,
Insensitive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchOutcome {
Match {
byte_index: usize,
},
Error(VimError),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Resource)]
pub struct SearchState {
active_query: Option<SearchQuery>,
active_direction: Option<SearchDirection>,
last_query: Option<SearchQuery>,
last_direction: Option<SearchDirection>,
last_outcome: Option<SearchOutcome>,
}
pub type VimSearchState = SearchState;
impl SearchState {
pub fn start(&mut self, direction: SearchDirection) {
self.active_query = Some(SearchQuery::new());
self.active_direction = Some(direction);
self.last_outcome = None;
}
pub fn cancel(&mut self) {
self.active_query = None;
self.active_direction = None;
}
#[must_use]
pub const fn is_active(&self) -> bool {
self.active_query.is_some()
}
#[must_use]
pub const fn active_query(&self) -> Option<&SearchQuery> {
self.active_query.as_ref()
}
#[must_use]
pub const fn active_direction(&self) -> Option<SearchDirection> {
self.active_direction
}
#[must_use]
pub const fn last_outcome(&self) -> Option<SearchOutcome> {
self.last_outcome
}
#[must_use]
pub const fn last_query(&self) -> Option<&SearchQuery> {
self.last_query.as_ref()
}
#[must_use]
pub const fn last_direction(&self) -> Option<SearchDirection> {
self.last_direction
}
pub fn push_text(&mut self, text: &str) {
if let Some(query) = &mut self.active_query {
query.push_str(text);
}
}
pub fn backspace(&mut self) {
if let Some(query) = &mut self.active_query {
query.pop_char();
}
}
pub fn submit(&mut self, text: &str, cursor_byte_index: usize) -> SearchOutcome {
let Some(query) = self.active_query.take() else {
return self.fail(VimError::NoPreviousRegularExpression);
};
let direction = self
.active_direction
.take()
.unwrap_or(SearchDirection::Forward);
let submitted_query = if query.is_empty() {
let Some(last_query) = self.last_query.clone() else {
return self.fail(VimError::NoPreviousRegularExpression);
};
last_query
} else {
self.last_query = Some(query.clone());
query
};
self.last_direction = Some(direction);
self.search_for_query(text, cursor_byte_index, &submitted_query, direction)
}
pub fn repeat(
&mut self,
text: &str,
cursor_byte_index: usize,
direction: SearchDirection,
) -> SearchOutcome {
let Some(query) = &self.last_query else {
return self.fail(VimError::NoPreviousRegularExpression);
};
let query = query.clone();
self.last_direction = Some(direction);
self.search_for_query(text, cursor_byte_index, &query, direction)
}
pub fn repeat_forward(&mut self, text: &str, cursor_byte_index: usize) -> SearchOutcome {
self.repeat(text, cursor_byte_index, SearchDirection::Forward)
}
pub fn repeat_backward(&mut self, text: &str, cursor_byte_index: usize) -> SearchOutcome {
self.repeat(text, cursor_byte_index, SearchDirection::Backward)
}
pub fn repeat_relative(
&mut self,
text: &str,
cursor_byte_index: usize,
repeat_direction: SearchRepeatDirection,
) -> SearchOutcome {
let Some(last_direction) = self.last_direction else {
return self.fail(VimError::NoPreviousRegularExpression);
};
let direction = match repeat_direction {
SearchRepeatDirection::Next => last_direction,
SearchRepeatDirection::Previous => last_direction.reversed(),
};
self.repeat(text, cursor_byte_index, direction)
}
fn search_for_query(
&mut self,
text: &str,
cursor_byte_index: usize,
query: &SearchQuery,
direction: SearchDirection,
) -> SearchOutcome {
let outcome = find_literal(text, cursor_byte_index, query, direction, true).map_or(
SearchOutcome::Error(VimError::PatternNotFound),
|byte_index| SearchOutcome::Match { byte_index },
);
self.last_outcome = Some(outcome);
outcome
}
const fn fail(&mut self, error: VimError) -> SearchOutcome {
let outcome = SearchOutcome::Error(error);
self.last_outcome = Some(outcome);
outcome
}
}
impl SearchDirection {
#[must_use]
pub const fn reversed(self) -> Self {
match self {
Self::Forward => Self::Backward,
Self::Backward => Self::Forward,
}
}
}
#[must_use]
pub fn find_literal(
text: &str,
cursor_byte_index: usize,
query: &SearchQuery,
direction: SearchDirection,
wrap: bool,
) -> Option<usize> {
if text.is_empty() || query.is_empty() {
return None;
}
match direction {
SearchDirection::Forward => find_forward(text, cursor_byte_index, query, wrap),
SearchDirection::Backward => find_backward(text, cursor_byte_index, query, wrap),
}
}
#[must_use]
pub fn literal_match_ranges_in_range(
text: &str,
query: &SearchQuery,
visible_range: std::ops::Range<usize>,
) -> Vec<std::ops::Range<usize>> {
let Some(matcher) = LiteralMatcher::new(query) else {
return Vec::new();
};
let start = clamp_to_boundary(text, visible_range.start.min(text.len()));
let end = clamp_to_boundary(text, visible_range.end.min(text.len()));
let mut ranges = Vec::new();
for byte_index in text[start..end]
.char_indices()
.map(|(offset, _character)| start + offset)
{
if let Some(match_end) = matcher.match_end(text, byte_index)
&& match_end > visible_range.start
&& byte_index < visible_range.end
{
ranges.push(byte_index..match_end);
}
}
ranges
}
fn find_forward(
text: &str,
cursor_byte_index: usize,
query: &SearchQuery,
wrap: bool,
) -> Option<usize> {
let start = next_search_start(text, cursor_byte_index);
let matcher = LiteralMatcher::new(query)?;
find_first_match_at_or_after(text, start, &matcher)
.or_else(|| {
wrap.then(|| find_first_match_before(text, start, &matcher))
.flatten()
})
.map(|byte_index| clamp_to_cursor_position(text, byte_index))
}
fn find_backward(
text: &str,
cursor_byte_index: usize,
query: &SearchQuery,
wrap: bool,
) -> Option<usize> {
let start = clamp_to_boundary(text, cursor_byte_index);
let matcher = LiteralMatcher::new(query)?;
find_last_match_before(text, start, &matcher)
.or_else(|| {
wrap.then(|| find_last_match_at_or_after(text, start, &matcher))
.flatten()
})
.map(|byte_index| clamp_to_cursor_position(text, byte_index))
}
struct LiteralMatcher {
query_chars: Vec<char>,
case_sensitivity: SearchCaseSensitivity,
}
impl LiteralMatcher {
fn new(query: &SearchQuery) -> Option<Self> {
(!query.is_empty()).then(|| Self {
query_chars: query.as_str().chars().collect(),
case_sensitivity: query.case_sensitivity(),
})
}
fn matches_at(&self, text: &str, byte_index: usize) -> bool {
self.match_end(text, byte_index).is_some()
}
fn match_end(&self, text: &str, byte_index: usize) -> Option<usize> {
if !text.is_char_boundary(byte_index) {
return None;
}
let mut text_characters = text[byte_index..].chars();
let mut end = byte_index;
for query_character in self.query_chars.iter().copied() {
let text_character = text_characters.next()?;
if !characters_match(text_character, query_character, self.case_sensitivity) {
return None;
}
end += text_character.len_utf8();
}
Some(end)
}
}
fn find_first_match_at_or_after(
text: &str,
start: usize,
matcher: &LiteralMatcher,
) -> Option<usize> {
text[start..]
.char_indices()
.map(|(offset, _character)| start + offset)
.find(|byte_index| matcher.matches_at(text, *byte_index))
}
fn find_first_match_before(text: &str, start: usize, matcher: &LiteralMatcher) -> Option<usize> {
text.char_indices()
.map(|(byte_index, _character)| byte_index)
.take_while(|byte_index| *byte_index < start)
.find(|byte_index| matcher.matches_at(text, *byte_index))
}
fn find_last_match_before(text: &str, start: usize, matcher: &LiteralMatcher) -> Option<usize> {
text.char_indices()
.map(|(byte_index, _character)| byte_index)
.take_while(|byte_index| *byte_index < start)
.filter(|byte_index| matcher.matches_at(text, *byte_index))
.last()
}
fn find_last_match_at_or_after(
text: &str,
start: usize,
matcher: &LiteralMatcher,
) -> Option<usize> {
text[start..]
.char_indices()
.map(|(offset, _character)| start + offset)
.rfind(|byte_index| matcher.matches_at(text, *byte_index))
}
fn characters_match(
text_character: char,
query_character: char,
case_sensitivity: SearchCaseSensitivity,
) -> bool {
match case_sensitivity {
SearchCaseSensitivity::Sensitive => text_character == query_character,
SearchCaseSensitivity::Insensitive => text_character
.to_lowercase()
.eq(query_character.to_lowercase()),
}
}
fn next_search_start(text: &str, cursor_byte_index: usize) -> usize {
let cursor = clamp_to_cursor_position(text, cursor_byte_index);
text[cursor..]
.chars()
.next()
.map_or(cursor, |character| cursor + character.len_utf8())
.min(text.len())
}
#[cfg(test)]
mod tests {
use super::{
SearchCaseSensitivity, SearchDirection, SearchOutcome, SearchQuery, SearchRepeatDirection,
SearchState, find_literal,
};
use crate::vim::VimError;
#[test]
fn forward_search_finds_match_after_cursor() {
let mut query = SearchQuery::new();
query.push_str("two");
assert_eq!(
find_literal("one two one two", 0, &query, SearchDirection::Forward, true),
Some("one ".len())
);
}
#[test]
fn forward_search_wraps_to_first_match() {
let mut query = SearchQuery::new();
query.push_str("one");
assert_eq!(
find_literal(
"one two",
"one two".len(),
&query,
SearchDirection::Forward,
true,
),
Some(0)
);
}
#[test]
fn forward_search_wrap_can_find_match_starting_at_cursor() {
let mut query = SearchQuery::new();
query.push_str("ALMA");
assert_eq!(
find_literal("ALMA", 0, &query, SearchDirection::Forward, true),
Some(0)
);
}
#[test]
fn backward_search_finds_previous_match_and_wraps() {
let mut query = SearchQuery::new();
query.push_str("two");
assert_eq!(
find_literal(
"one two one two",
"one two one".len(),
&query,
SearchDirection::Backward,
true,
),
Some("one ".len())
);
assert_eq!(
find_literal("one two", 0, &query, SearchDirection::Backward, true),
Some("one ".len())
);
}
#[test]
fn empty_or_missing_query_does_not_move() {
let query = SearchQuery::new();
let mut state = SearchState::default();
assert_eq!(
find_literal("abc", 0, &query, SearchDirection::Forward, true),
None
);
assert_eq!(
state.repeat_forward("abc", 0),
SearchOutcome::Error(VimError::NoPreviousRegularExpression)
);
}
#[test]
fn lowercase_queries_match_case_insensitively() {
let mut query = SearchQuery::new();
query.push_str("alma");
assert_eq!(query.case_sensitivity(), SearchCaseSensitivity::Insensitive);
assert_eq!(
find_literal("ALMA alma", 0, &query, SearchDirection::Forward, true),
Some("ALMA ".len())
);
assert_eq!(
find_literal("ALMA", 0, &query, SearchDirection::Forward, true),
Some(0)
);
}
#[test]
fn uppercase_queries_match_case_sensitively() {
let mut query = SearchQuery::new();
query.push_str("Alma");
assert_eq!(query.case_sensitivity(), SearchCaseSensitivity::Sensitive);
assert_eq!(
find_literal("ALMA alva", 0, &query, SearchDirection::Forward, true),
None
);
}
#[test]
fn unicode_search_stays_boundary_safe() {
let mut query = SearchQuery::new();
query.push_str("λ");
assert_eq!(
find_literal("Aλ Bλ", 0, &query, SearchDirection::Forward, true),
Some("A".len())
);
}
#[test]
fn repeat_keys_reuse_last_submitted_query() {
let mut state = SearchState::default();
state.start(SearchDirection::Forward);
state.push_text("two");
assert_eq!(
state.submit("one two one two", 0),
SearchOutcome::Match {
byte_index: "one ".len()
}
);
assert_eq!(
state.repeat_forward("one two one two", "one two".len()),
SearchOutcome::Match {
byte_index: "one two one ".len()
}
);
assert_eq!(
state.repeat_backward("one two one two", "one two one ".len()),
SearchOutcome::Match {
byte_index: "one ".len()
}
);
}
#[test]
fn relative_repeat_keys_follow_last_search_direction() {
let text = "one two one two";
let mut state = SearchState::default();
state.start(SearchDirection::Backward);
state.push_text("two");
assert_eq!(
state.submit(text, text.len()),
SearchOutcome::Match {
byte_index: "one two one ".len()
}
);
assert_eq!(
state.repeat_relative(text, "one two one ".len(), SearchRepeatDirection::Next),
SearchOutcome::Match {
byte_index: "one ".len()
}
);
assert_eq!(
state.repeat_relative(text, "one ".len(), SearchRepeatDirection::Previous),
SearchOutcome::Match {
byte_index: "one two one ".len()
}
);
}
#[test]
fn empty_submit_repeats_previous_search_or_reports_e35() {
let mut state = SearchState::default();
state.start(SearchDirection::Forward);
assert_eq!(
state.submit("one two", 0),
SearchOutcome::Error(VimError::NoPreviousRegularExpression)
);
state.start(SearchDirection::Forward);
state.push_text("two");
assert_eq!(
state.submit("one two one two", 0),
SearchOutcome::Match {
byte_index: "one ".len()
}
);
state.start(SearchDirection::Forward);
assert_eq!(
state.submit("one two one two", "one ".len()),
SearchOutcome::Match {
byte_index: "one two one ".len()
}
);
}
#[test]
fn literal_match_ranges_cover_visible_occurrences() {
let mut query = SearchQuery::new();
query.push_str("alma");
assert_eq!(
super::literal_match_ranges_in_range("ALMA alma λ", &query, 0.."ALMA alma λ".len()),
vec![0..4, 5..9]
);
}
}