#![allow(dead_code, non_snake_case)]
use crate::ported::buffer::{curbuf, ml_get};
use crate::ported::eval::eval_expr_to_bool;
use crate::ported::eval::typval_defs_h::typval_T;
use crate::ported::eval_h::FAIL;
use crate::ported::window::{colnr_T, curwin, linenr_T, pos_T};
use crate::viml_regex::Regex;
use std::cell::RefCell;
pub const FORWARD: i32 = 1;
pub const BACKWARD: i32 = -1;
pub const SEARCH_END: i32 = 0x40; pub const SEARCH_START: i32 = 0x100; pub const SEARCH_KEEP: i32 = 0x400; pub const SEARCH_COL: i32 = 0x1000;
pub const RE_SEARCH: i32 = 0;
thread_local! {
pub static p_ws: RefCell<bool> = const { RefCell::new(true) };
}
pub fn searchit(pos: &mut pos_T, dir: i32, pat: &str, options: i32, stop_lnum: linenr_T) -> i32 {
let char_byte_offsets = |line: &str| -> Vec<colnr_T> {
let mut v: Vec<colnr_T> = line.char_indices().map(|(i, _)| i as colnr_T).collect();
v.push(line.len() as colnr_T);
v
};
let byte_to_char = |byte_of_char: &[colnr_T], col: colnr_T| -> usize {
match byte_of_char.iter().position(|&b| b >= col) {
Some(i) => i,
None => byte_of_char.len().saturating_sub(1),
}
};
if pat.is_empty() {
return FAIL;
}
let re = Regex::compile(pat);
let line_count = curbuf
.with(|c| c.borrow().clone())
.map_or(0, |b| b.borrow().b_ml.ml_line_count);
if line_count == 0 {
return FAIL;
}
let ic = crate::ported::eval::typval::tv_get_number(&crate::ported::option::get_option_value(
"ignorecase",
)) != 0;
let accept_start = (options & SEARCH_START) != 0;
let want_end = (options & SEARCH_END) != 0;
let start_lnum = pos.lnum.clamp(1, line_count);
let start_col = pos.col.max(0);
let mut passes = 0;
let mut lnum = start_lnum;
loop {
let line = ml_get(lnum);
let chars: Vec<char> = line.chars().collect();
let byte_of_char = char_byte_offsets(&line);
if dir == FORWARD {
let from_char = if lnum == start_lnum && passes == 0 {
let sc = byte_to_char(&byte_of_char, start_col);
if accept_start {
sc
} else {
sc + 1
}
} else {
0
};
if from_char <= chars.len() {
if let Some(cap) = re.find_from(&chars, ic, from_char) {
let (s, e) = cap.whole();
let col_char = if want_end { e.saturating_sub(1) } else { s };
pos.lnum = lnum;
pos.col = byte_of_char.get(col_char).copied().unwrap_or(0);
return 1;
}
}
} else {
let upto_char = if lnum == start_lnum && passes == 0 {
let sc = byte_to_char(&byte_of_char, start_col);
if accept_start {
sc + 1
} else {
sc
}
} else {
chars.len() + 1
};
let mut best: Option<(usize, usize)> = None;
let mut from = 0;
while from <= chars.len() {
match re.find_from(&chars, ic, from) {
Some(cap) => {
let (s, e) = cap.whole();
if s < upto_char {
best = Some((s, e));
from = s + 1; } else {
break;
}
}
None => break,
}
}
if let Some((s, e)) = best {
let col_char = if want_end { e.saturating_sub(1) } else { s };
pos.lnum = lnum;
pos.col = byte_of_char.get(col_char).copied().unwrap_or(0);
return 1;
}
}
if dir == FORWARD {
if stop_lnum != 0 && lnum >= stop_lnum {
if passes == 0 && p_ws.with(|w| *w.borrow()) {
} else {
return FAIL;
}
}
lnum += 1;
if lnum > line_count {
if passes == 0 && p_ws.with(|w| *w.borrow()) {
passes = 1;
lnum = 1;
} else {
return FAIL;
}
}
if passes == 1 && lnum > start_lnum {
return FAIL;
}
} else {
if stop_lnum != 0 && lnum <= stop_lnum {
if passes == 0 && p_ws.with(|w| *w.borrow()) {
} else {
return FAIL;
}
}
lnum -= 1;
if lnum < 1 {
if passes == 0 && p_ws.with(|w| *w.borrow()) {
passes = 1;
lnum = line_count;
} else {
return FAIL;
}
}
if passes == 1 && lnum < start_lnum {
return FAIL;
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn do_searchpair(
spat: &str,
mpat: &str,
epat: &str,
dir: i32,
skip: Option<&typval_T>,
flags: i32,
match_pos: Option<&mut pos_T>,
lnum_stop: linenr_T,
_time_limit: i64,
) -> i32 {
let char_byte_offsets = |line: &str| -> Vec<colnr_T> {
let mut v: Vec<colnr_T> = line.char_indices().map(|(i, _)| i as colnr_T).collect();
v.push(line.len() as colnr_T);
v
};
let byte_to_char = |byte_of_char: &[colnr_T], col: colnr_T| -> usize {
match byte_of_char.iter().position(|&b| b >= col) {
Some(i) => i,
None => byte_of_char.len().saturating_sub(1),
}
};
let set_cursor = |pos: pos_T| {
if let Some(w) = curwin.with(|c| c.borrow().clone()) {
w.borrow_mut().w_cursor = pos;
}
};
let mut retval = 0; let mut nest = 1;
let use_skip = skip.is_some_and(crate::ported::eval::eval_expr_valid_arg);
let mut options = SEARCH_KEEP;
if (flags & 0x10) != 0 {
options |= SEARCH_START;
}
let save_cursor = curwin
.with(|c| c.borrow().clone())
.map_or(pos_T::default(), |w| w.borrow().w_cursor);
let mut pos = save_cursor;
let mut firstpos = pos_T::default(); let mut foundpos = pos_T::default();
let re_open = Regex::compile(if dir == BACKWARD { epat } else { spat });
let re_close = Regex::compile(if dir == BACKWARD { spat } else { epat });
let re_mid = if mpat.is_empty() {
None
} else {
Some(Regex::compile(mpat))
};
let ic = crate::ported::eval::typval::tv_get_number(&crate::ported::option::get_option_value(
"ignorecase",
)) != 0;
let combined = if mpat.is_empty() {
format!("\\({spat}\\)\\|\\({epat}\\)")
} else {
format!("\\({spat}\\)\\|\\({epat}\\)\\|\\({mpat}\\)")
};
loop {
let n = searchit(&mut pos, dir, &combined, options, lnum_stop);
if n == FAIL || (firstpos.lnum != 0 && pos == firstpos) {
break;
}
if firstpos.lnum == 0 {
firstpos = pos; }
if pos == foundpos {
if dir == BACKWARD {
if pos.col > 0 {
pos.col -= 1;
} else if pos.lnum > 1 {
pos.lnum -= 1;
pos.col = ml_get(pos.lnum).len() as colnr_T;
}
} else {
pos.col += 1;
}
}
foundpos = pos;
options &= !SEARCH_START;
if use_skip {
let save = save_cursor;
set_cursor(pos);
let do_skip = skip.map(eval_expr_to_bool).unwrap_or(false);
set_cursor(save);
if do_skip {
continue;
}
}
let line = ml_get(pos.lnum);
let chars: Vec<char> = line.chars().collect();
let byte_of_char = char_byte_offsets(&line);
let at = byte_to_char(&byte_of_char, pos.col);
let is_close = re_close
.find_from(&chars, ic, at)
.is_some_and(|c| c.whole().0 == at);
let is_open = re_open
.find_from(&chars, ic, at)
.is_some_and(|c| c.whole().0 == at);
let is_mid = re_mid
.as_ref()
.and_then(|r| r.find_from(&chars, ic, at))
.is_some_and(|c| c.whole().0 == at);
if is_close && !is_open {
nest -= 1;
if nest == 0 {
retval = 1; break;
}
} else if is_open {
nest += 1; } else if is_mid && nest == 1 {
retval = 1; break;
}
}
if retval != 0 {
if let Some(mp) = match_pos {
*mp = pos;
}
if (flags & 0x01) != 0 {
set_cursor(save_cursor);
} else {
set_cursor(pos);
}
} else {
set_cursor(save_cursor);
}
retval
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::buffer::{buflist_new, curbuf, firstbuf, lastbuf, top_file_num, BLN_LISTED};
fn setup_buffer(lines: &[&str]) {
firstbuf.with(|f| *f.borrow_mut() = None);
lastbuf.with(|l| *l.borrow_mut() = None);
curbuf.with(|c| *c.borrow_mut() = None);
top_file_num.with(|t| t.set(1));
let buf = buflist_new(Some("/tmp/s".into()), None, 0, BLN_LISTED).unwrap();
{
let mut b = buf.borrow_mut();
b.b_ml.ml_lines = lines.iter().map(|s| s.to_string()).collect();
b.b_ml.ml_line_count = lines.len() as linenr_T;
b.b_ml.ml_mfp = true;
}
curbuf.with(|c| *c.borrow_mut() = Some(buf));
}
#[test]
fn searchit_forward_finds_next() {
setup_buffer(&["foo bar", "baz foo", "qux"]);
p_ws.with(|w| *w.borrow_mut() = false);
let mut pos = pos_T {
lnum: 1,
col: 0,
coladd: 0,
};
let r = searchit(&mut pos, FORWARD, "foo", 0, 0);
assert_eq!(r, 1);
assert_eq!((pos.lnum, pos.col), (2, 4));
}
#[test]
fn searchit_forward_accept_start() {
setup_buffer(&["foo bar"]);
p_ws.with(|w| *w.borrow_mut() = false);
let mut pos = pos_T {
lnum: 1,
col: 0,
coladd: 0,
};
let r = searchit(&mut pos, FORWARD, "foo", SEARCH_START, 0);
assert_eq!(r, 1);
assert_eq!((pos.lnum, pos.col), (1, 0));
}
#[test]
fn searchit_no_match_returns_fail() {
setup_buffer(&["abc", "def"]);
p_ws.with(|w| *w.borrow_mut() = true);
let mut pos = pos_T {
lnum: 1,
col: 0,
coladd: 0,
};
assert_eq!(searchit(&mut pos, FORWARD, "zzz", 0, 0), FAIL);
}
#[test]
fn searchit_backward_finds_prev() {
setup_buffer(&["foo", "bar", "foo x"]);
p_ws.with(|w| *w.borrow_mut() = false);
let mut pos = pos_T {
lnum: 3,
col: 4,
coladd: 0,
};
let r = searchit(&mut pos, BACKWARD, "foo", 0, 0);
assert_eq!(r, 1);
assert_eq!((pos.lnum, pos.col), (3, 0));
}
}