use std::sync::{LazyLock, Once};
use duat_core::{
Ns, buffer::Buffer, context::{self, Handle}, data::Pass, form::{self, Form}, hook, text::{Text, txt}, ui::{PrintInfo, RwArea}
};
use crate::{
hooks::{SearchPerformed, SearchUpdated},
modes::{Prompt, PromptMode},
};
static NS: LazyLock<Ns> = LazyLock::new(Ns::new);
pub struct IncSearch<I: IncSearcher> {
inc: I,
orig: Option<(duat_core::mode::Selections, PrintInfo)>,
prev: String,
}
impl<I: IncSearcher> Clone for IncSearch<I> {
fn clone(&self) -> Self {
Self {
inc: self.inc.clone(),
orig: self.orig.clone(),
prev: self.prev.clone(),
}
}
}
impl<I: IncSearcher> IncSearch<I> {
#[allow(clippy::new_ret_no_self)]
pub fn new(inc: I) -> Prompt {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
form::set_weak("regex.error", Form::mimic("accent.error"));
form::set_weak("regex.operator", Form::mimic("operator"));
form::set_weak("regex.class", Form::mimic("constant"));
form::set_weak("regex.bracket", Form::mimic("punctuation.bracket"));
});
Prompt::new(Self { inc, orig: None, prev: String::new() })
}
}
impl<I: IncSearcher> PromptMode for IncSearch<I> {
type ExitWidget = Buffer;
fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
text.remove_tags(*NS, ..);
let handle = context::current_buffer(pa);
if text == self.prev {
return text;
} else {
let prev = std::mem::replace(&mut self.prev, text.to_string_no_last_nl());
hook::trigger(pa, SearchUpdated((prev, self.prev.clone())));
}
let pat = text.to_string_no_last_nl();
match regex_syntax::parse(&pat) {
Ok(_) => {
handle.area().set_print_info(pa, orig_print_info.clone());
let buffer = handle.write(pa);
*buffer.selections_mut() = orig_selections.clone();
let ast = regex_syntax::ast::parse::Parser::new()
.parse(&text.to_string_no_last_nl())
.unwrap();
crate::tag_from_ast(*NS, &mut text, &ast);
if !text.is_empty() {
self.inc.search(pa, &pat, handle);
}
}
Err(err) => {
let regex_syntax::Error::Parse(err) = err else {
unreachable!("As far as I can tell, regex_syntax has goofed up");
};
let span = err.span();
let id = form::id_of!("regex.error");
text.insert_tag(*NS, span.start.offset..span.end.offset, id.to_tag(0));
}
}
text
}
fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &RwArea) -> Text {
let handle = context::current_buffer(pa);
self.orig = Some((
handle.read(pa).selections().clone(),
handle.area().get_print_info(pa),
));
text
}
fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
if !text.is_empty() {
let pat = text.to_string_no_last_nl();
if let Err(err) = regex_syntax::parse(&pat) {
let regex_syntax::Error::Parse(err) = err else {
unreachable!("As far as I can tell, regex_syntax has goofed up");
};
let range = err.span().start.offset..err.span().end.offset;
let err = txt!(
"[a]{:?}, \"{}\"[prompt.colon]:[] {}",
range,
&text[range],
err.kind()
);
context::error!("{err}")
} else {
hook::trigger(pa, SearchPerformed(pat));
}
}
}
fn prompt(&self) -> Text {
txt!("{}", self.inc.prompt())
}
}
pub trait IncSearcher: Clone + Send + 'static {
fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>);
fn prompt(&self) -> Text;
}
#[derive(Clone, Copy)]
pub struct SearchFwd;
impl IncSearcher for SearchFwd {
fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
handle.edit_all(pa, |mut c| {
if let Some(range) = {
c.search(pat).from_caret_excl().next().or_else(|| {
context::info!("search wrapped around buffer");
c.search(pat).to_caret().next()
})
} {
c.move_to(range)
}
});
}
fn prompt(&self) -> Text {
txt!("[prompt]search")
}
}
#[derive(Clone, Copy)]
pub struct SearchRev;
impl IncSearcher for SearchRev {
fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
handle.edit_all(pa, |mut c| {
if let Some(range) = {
c.search(pat).to_caret().next_back().or_else(|| {
context::info!("search wrapped around buffer");
c.search(pat).from_caret_excl().next_back()
})
} {
c.move_to(range)
}
});
}
fn prompt(&self) -> Text {
txt!("[prompt]rev search")
}
}
#[derive(Clone, Copy)]
pub struct ExtendFwd;
impl IncSearcher for ExtendFwd {
fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
handle.edit_all(pa, |mut c| {
if let Some(range) = {
c.search(pat).from_caret_excl().next().or_else(|| {
context::info!("search wrapped around buffer");
c.search(pat).to_caret().next()
})
} {
c.set_anchor_if_needed();
c.move_to(range)
}
});
}
fn prompt(&self) -> Text {
txt!("[prompt]search (extend)")
}
}
#[derive(Clone, Copy)]
pub struct ExtendRev;
impl IncSearcher for ExtendRev {
fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
handle.edit_all(pa, |mut c| {
if let Some(range) = {
c.search(pat).to_caret().next_back().or_else(|| {
context::info!("search wrapped around buffer");
c.search(pat).from_caret_excl().next_back()
})
} {
c.set_anchor_if_needed();
c.move_to(range)
}
});
}
fn prompt(&self) -> Text {
txt!("[prompt]rev search (extend)")
}
}