use hjkl_engine::{Host, Input, Key};
pub fn step_search_prompt<H: Host>(
ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
input: Input,
) -> bool {
let history_dir = match (input.key, input.ctrl) {
(Key::Char('p'), true) | (Key::Up, _) => Some(-1isize),
(Key::Char('n'), true) | (Key::Down, _) => Some(1isize),
_ => None,
};
if let Some(dir) = history_dir {
ed.walk_search_history(dir);
return true;
}
match input.key {
Key::Esc => {
let text = ed
.take_search_prompt_state()
.map(|p| p.text)
.unwrap_or_default();
if !text.is_empty() {
ed.set_last_search_pattern_only(Some(text));
}
ed.set_search_history_cursor(None);
}
Key::Enter => {
let prompt = ed.take_search_prompt_state();
if let Some(p) = prompt {
let delim = if p.forward { '/' } else { '?' };
let (pat_text, offset) = split_search_offset(&p.text, delim);
let pattern: Option<String> = if pat_text.is_empty() {
ed.last_search_pattern().map(str::to_owned)
} else {
Some(pat_text)
};
if let Some(pattern) = pattern {
ed.push_search_pattern(&pattern);
let pre = ed.cursor();
if p.forward {
ed.search_advance_forward(true);
} else {
ed.search_advance_backward(true);
}
if let Some(off) = offset.as_deref().filter(|s| !s.is_empty()) {
apply_search_offset(ed, off);
}
ed.push_buffer_cursor_to_textarea();
if let Some((op, _count, origin)) = p.operator {
ed.apply_op_search_range(op, origin);
} else if ed.cursor() != pre {
ed.push_jump(pre);
}
ed.record_search_history(&pattern);
ed.set_last_search_pattern_only(Some(pattern));
ed.set_last_search_forward_only(p.forward);
}
}
ed.set_search_history_cursor(None);
}
Key::Backspace => {
ed.set_search_history_cursor(None);
let new_text = ed.search_prompt_state_mut().and_then(|p| {
if p.text.pop().is_some() {
p.cursor = p.text.chars().count();
Some(p.text.clone())
} else {
None
}
});
if let Some(text) = new_text {
ed.push_search_pattern(&text);
}
}
Key::Char(c) => {
ed.set_search_history_cursor(None);
let new_text = ed.search_prompt_state_mut().map(|p| {
p.text.push(c);
p.cursor = p.text.chars().count();
p.text.clone()
});
if let Some(text) = new_text {
ed.push_search_pattern(&text);
}
}
_ => {}
}
true
}
fn split_search_offset(text: &str, delim: char) -> (String, Option<String>) {
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' {
i += 2;
continue;
}
if chars[i] == delim {
let pat: String = chars[..i].iter().collect();
let off: String = chars[i + 1..].iter().collect();
return (pat, Some(off));
}
i += 1;
}
(text.to_string(), None)
}
fn apply_search_offset<H: Host>(
ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
offset: &str,
) {
let (row, col) = ed.cursor();
match offset.chars().next() {
Some('e') => {
let n: isize = offset[1..].parse().unwrap_or(0);
let len = match_len_at(ed, row, col) as isize;
let end = col as isize + (len - 1).max(0) + n;
ed.jump_cursor(row, end.max(0) as usize);
}
Some('s') | Some('b') => {
let n: isize = offset[1..].parse().unwrap_or(0);
ed.jump_cursor(row, (col as isize + n).max(0) as usize);
}
_ => {
let n: isize = offset.parse().unwrap_or(0);
let rope = ed.buffer().rope();
let last = rope.len_lines().saturating_sub(1);
let new_row = (row as isize + n).clamp(0, last as isize) as usize;
let line = hjkl_buffer::rope_line_str(&rope, new_row);
let fnb = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
drop(rope);
ed.jump_cursor(new_row, fnb);
}
}
}
fn match_len_at<H: Host>(
ed: &hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
row: usize,
col: usize,
) -> usize {
let Some(re) = ed.search_state().pattern.as_ref() else {
return 1;
};
let line = hjkl_buffer::rope_line_str(&ed.buffer().rope(), row);
let byte_col = line
.char_indices()
.nth(col)
.map(|(b, _)| b)
.unwrap_or(line.len());
if let Some(m) = re.find(&line[byte_col..])
&& m.start() == 0
{
return line[byte_col..byte_col + m.end()].chars().count();
}
1
}
#[cfg(test)]
mod offset_tests {
use super::split_search_offset;
#[test]
fn plain_pattern_no_offset() {
assert_eq!(split_search_offset("bar", '/'), ("bar".into(), None));
}
#[test]
fn word_boundary_pattern_preserved() {
assert_eq!(
split_search_offset("\\<bar\\>", '/'),
("\\<bar\\>".into(), None)
);
}
#[test]
fn trailing_delim_empty_offset() {
assert_eq!(
split_search_offset("bar/", '/'),
("bar".into(), Some(String::new()))
);
}
#[test]
fn end_offset() {
assert_eq!(
split_search_offset("bar/e", '/'),
("bar".into(), Some("e".into()))
);
}
#[test]
fn escaped_delim_not_split() {
assert_eq!(split_search_offset("a\\/b", '/'), ("a\\/b".into(), None));
}
}