use regex::RegexBuilder;
use rmux_proto::RmuxError;
use std::borrow::Cow;
use super::args::parse_single_argument;
use super::text::{
line_char, owner_positions, pattern_looks_like_regex, position_ge, position_le, LineTextMap,
};
use super::types::{
ClearPolicy, CopyModeCommandOutcome, JumpKind, JumpState, ModeKeys, SearchDirection,
};
use super::CopyModeState;
const SEARCH_REGEX_SIZE_LIMIT: usize = 1_000_000;
const SEARCH_REGEX_DFA_SIZE_LIMIT: usize = 1_000_000;
const SEARCH_OFF_LOCK_LINE_THRESHOLD: usize = 10_000;
const SEARCH_OFF_LOCK_ARG_BYTES_THRESHOLD: usize = 4096;
impl CopyModeState {
pub(crate) fn command_runs_search(command: &str) -> bool {
matches!(
command,
"search-again"
| "search-backward"
| "search-backward-text"
| "search-backward-incremental"
| "search-forward"
| "search-forward-text"
| "search-forward-incremental"
| "search-reverse"
)
}
pub(crate) fn mark_search_timed_out(&mut self) {
self.search_timed_out = true;
self.search_count_partial = true;
}
pub(crate) fn search_should_run_off_lock(&self, args: &[String]) -> bool {
let arg_bytes = args.iter().map(String::len).sum::<usize>();
self.total_lines() > SEARCH_OFF_LOCK_LINE_THRESHOLD
|| arg_bytes > SEARCH_OFF_LOCK_ARG_BYTES_THRESHOLD
}
pub(super) fn search_with_arg(
&mut self,
args: &[String],
direction: SearchDirection,
plain_text: bool,
) -> Result<CopyModeCommandOutcome, RmuxError> {
let pattern = parse_single_argument("search", args)?;
self.perform_search(pattern, direction, plain_text)?;
let outcome = self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always);
self.search_highlighted = !self.search_results.is_empty();
Ok(outcome)
}
pub(super) fn search_again(&mut self) -> Result<CopyModeCommandOutcome, RmuxError> {
if self.search_pattern.is_empty() || self.search_results.is_empty() {
return Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always));
}
self.advance_search_match(self.search_direction);
Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always))
}
pub(super) fn search_reverse(&mut self) -> Result<CopyModeCommandOutcome, RmuxError> {
if self.search_pattern.is_empty() || self.search_results.is_empty() {
return Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always));
}
let reverse = match self.search_direction {
SearchDirection::Forward => SearchDirection::Backward,
SearchDirection::Backward => SearchDirection::Forward,
};
self.advance_search_match(reverse);
Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always))
}
pub(super) fn incremental_search(
&mut self,
args: &[String],
default_direction: SearchDirection,
) -> Result<CopyModeCommandOutcome, RmuxError> {
let value = parse_single_argument("incremental-search", args)?;
let (prefix, pattern) = match value.split_once(':') {
Some((prefix, pattern)) => (prefix, pattern),
None => ("=", value.as_str()),
};
if prefix.is_empty() {
return Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always));
}
let direction = match prefix {
"+" => SearchDirection::Forward,
"-" => SearchDirection::Backward,
"=" => self.search_direction,
_ => default_direction,
};
if pattern.is_empty() {
self.search_pattern.clear();
self.search_results.clear();
self.search_current = None;
self.search_highlighted = false;
return Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always));
}
let plain = !pattern_looks_like_regex(pattern);
self.perform_search(pattern.to_owned(), direction, plain)?;
let outcome = self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Always);
self.search_highlighted = !self.search_results.is_empty();
Ok(outcome)
}
pub(super) fn jump_with_arg(
&mut self,
args: &[String],
kind: JumpKind,
) -> Result<CopyModeCommandOutcome, RmuxError> {
let arg = parse_single_argument("jump", args)?;
let ch = arg.chars().next().ok_or_else(|| {
RmuxError::Server("jump command expects a non-empty character".to_owned())
})?;
self.jump = Some(JumpState { kind, ch });
self.execute_jump(kind, ch)?;
Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::EmacsOnly))
}
pub(super) fn jump_again(
&mut self,
reverse: bool,
) -> Result<CopyModeCommandOutcome, RmuxError> {
let Some(jump) = self.jump.clone() else {
return Ok(
self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::EmacsOnly)
);
};
let kind = if reverse {
jump.kind.reverse()
} else {
jump.kind
};
self.execute_jump(kind, jump.ch)?;
Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::EmacsOnly))
}
fn perform_search(
&mut self,
pattern: String,
direction: SearchDirection,
plain_text: bool,
) -> Result<(), RmuxError> {
self.search_pattern = pattern;
self.search_direction = direction;
self.search_timed_out = false;
self.search_count_partial = false;
self.rebuild_search_results(plain_text);
self.search_highlighted = true;
self.search_current = self.find_search_match(direction);
if let Some(index) = self.search_current {
if let Some(result) = self.search_results.get(index) {
self.cursor = match self.mode_keys {
ModeKeys::Vi => result.start,
ModeKeys::Emacs => result.end,
};
self.ensure_cursor_visible();
self.sync_selection_with_cursor();
}
}
Ok(())
}
pub(super) fn rebuild_search_results(&mut self, plain_text: bool) {
self.search_results.clear();
if self.search_pattern.is_empty() {
self.search_current = None;
return;
}
let case_insensitive = self
.search_pattern
.chars()
.all(|ch| !ch.is_ascii_uppercase());
let plain_needle = plain_text.then(|| {
if case_insensitive {
Cow::Owned(self.search_pattern.to_lowercase())
} else {
Cow::Borrowed(self.search_pattern.as_str())
}
});
let regex = if plain_text {
None
} else {
let mut builder = RegexBuilder::new(&self.search_pattern);
builder.case_insensitive(case_insensitive);
builder.size_limit(SEARCH_REGEX_SIZE_LIMIT);
builder.dfa_size_limit(SEARCH_REGEX_DFA_SIZE_LIMIT);
match builder.build() {
Ok(regex) => Some(regex),
Err(_) => {
self.search_count_partial = true;
return;
}
}
};
for y in 0..self.total_lines() {
let line = self.line(y);
let map = LineTextMap::new(&line);
if map.text.is_empty() {
continue;
}
if plain_text {
let needle = plain_needle
.as_ref()
.expect("plain search needle must be initialized");
let needle = needle.as_ref();
let lowercase = case_insensitive.then(|| map.lowercase());
let haystack = lowercase
.as_ref()
.map_or(map.text.as_str(), |lowercase| lowercase.text.as_str());
let mut offset = 0;
while let Some(found) = haystack[offset..].find(needle) {
let start = offset + found;
let end = start + needle.len();
let original_range = match &lowercase {
Some(lowercase) => lowercase.original_range(start..end),
None => Some(start..end),
};
if let Some(original_range) = original_range {
if let Some(result) = map.match_range(y, original_range) {
self.search_results.push(result);
}
}
offset = start.saturating_add(needle.len().max(1));
if offset >= haystack.len() {
break;
}
}
} else {
if let Some(regex) = ®ex {
for matched in regex.find_iter(&map.text) {
if let Some(result) = map.match_range(y, matched.start()..matched.end()) {
self.search_results.push(result);
}
}
}
}
}
}
fn advance_search_match(&mut self, direction: SearchDirection) {
if self.search_results.is_empty() {
return;
}
let current = self.search_current;
let next = match direction {
SearchDirection::Forward => {
let next = current
.and_then(|index| {
if index + 1 < self.search_results.len() {
Some(index + 1)
} else if self.wrap_search {
Some(0)
} else {
None
}
})
.or_else(|| self.find_search_match(direction));
if next.is_none() && current.is_some() {
return;
}
next
}
SearchDirection::Backward => {
let next = current
.and_then(|index| {
if index > 0 {
Some(index - 1)
} else if self.wrap_search {
Some(self.search_results.len() - 1)
} else {
None
}
})
.or_else(|| self.find_search_match(direction));
if next.is_none() && current.is_some() {
return;
}
next
}
};
self.search_current = next;
if let Some(index) = next {
if let Some(result) = self.search_results.get(index) {
self.cursor = match self.mode_keys {
ModeKeys::Vi => result.start,
ModeKeys::Emacs => result.end,
};
self.ensure_cursor_visible();
self.sync_selection_with_cursor();
}
}
self.search_highlighted = true;
}
fn find_search_match(&self, direction: SearchDirection) -> Option<usize> {
match direction {
SearchDirection::Forward => self
.search_results
.iter()
.enumerate()
.find(|(_, result)| position_ge(result.start, self.cursor))
.map(|(index, _)| index)
.or_else(|| (self.wrap_search && !self.search_results.is_empty()).then_some(0)),
SearchDirection::Backward => self
.search_results
.iter()
.enumerate()
.rev()
.find(|(_, result)| position_le(result.end, self.cursor))
.map(|(index, _)| index)
.or_else(|| {
self.wrap_search
.then(|| self.search_results.len().checked_sub(1))
.flatten()
}),
}
}
fn execute_jump(&mut self, kind: JumpKind, ch: char) -> Result<(), RmuxError> {
let line = self.line(self.cursor.y);
let positions = owner_positions(&line);
if positions.is_empty() {
return Ok(());
}
let current_owner = line.owning_cell_x(self.cursor.x).unwrap_or(0);
let found = match kind {
JumpKind::Forward => positions
.iter()
.copied()
.find(|x| *x > current_owner && line_char(&line, *x) == Some(ch)),
JumpKind::ToForward => positions
.iter()
.copied()
.find(|x| *x > current_owner.saturating_add(1) && line_char(&line, *x) == Some(ch)),
JumpKind::Backward => positions
.iter()
.rev()
.copied()
.find(|x| *x < current_owner && line_char(&line, *x) == Some(ch)),
JumpKind::ToBackward => positions.iter().rev().copied().find(|x| {
self.next_owner_in_line(&line, *x)
.is_some_and(|next| next < current_owner)
&& line_char(&line, *x) == Some(ch)
}),
};
if let Some(found) = found {
self.cursor.x = match kind {
JumpKind::Forward | JumpKind::Backward => found,
JumpKind::ToForward => previous_owner(&positions, found).unwrap_or(found),
JumpKind::ToBackward => self.next_owner_in_line(&line, found).unwrap_or(found),
};
self.ensure_cursor_visible();
self.sync_selection_with_cursor();
}
Ok(())
}
}
fn previous_owner(positions: &[u32], found: u32) -> Option<u32> {
positions.iter().rev().copied().find(|x| *x < found)
}