duat_base/modes/
inc_search.rs1use std::sync::{LazyLock, Once};
11
12use duat_core::{
13 Ns, buffer::Buffer, context::{self, Handle}, data::Pass, form::{self, Form}, hook, text::{Text, txt}, ui::{PrintInfo, RwArea}
14};
15
16use crate::{
17 hooks::{SearchPerformed, SearchUpdated},
18 modes::{Prompt, PromptMode},
19};
20
21static NS: LazyLock<Ns> = LazyLock::new(Ns::new);
22
23pub struct IncSearch<I: IncSearcher> {
48 inc: I,
49 orig: Option<(duat_core::mode::Selections, PrintInfo)>,
50 prev: String,
51}
52
53impl<I: IncSearcher> Clone for IncSearch<I> {
54 fn clone(&self) -> Self {
55 Self {
56 inc: self.inc.clone(),
57 orig: self.orig.clone(),
58 prev: self.prev.clone(),
59 }
60 }
61}
62
63impl<I: IncSearcher> IncSearch<I> {
64 #[allow(clippy::new_ret_no_self)]
67 pub fn new(inc: I) -> Prompt {
68 static ONCE: Once = Once::new();
69 ONCE.call_once(|| {
70 form::set_weak("regex.error", Form::mimic("accent.error"));
71 form::set_weak("regex.operator", Form::mimic("operator"));
72 form::set_weak("regex.class", Form::mimic("constant"));
73 form::set_weak("regex.bracket", Form::mimic("punctuation.bracket"));
74 });
75 Prompt::new(Self { inc, orig: None, prev: String::new() })
76 }
77}
78
79impl<I: IncSearcher> PromptMode for IncSearch<I> {
80 type ExitWidget = Buffer;
81
82 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
83 let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
84 text.remove_tags(*NS, ..);
85
86 let handle = context::current_buffer(pa);
87
88 if text == self.prev {
89 return text;
90 } else {
91 let prev = std::mem::replace(&mut self.prev, text.to_string_no_last_nl());
92 hook::trigger(pa, SearchUpdated((prev, self.prev.clone())));
93 }
94
95 let pat = text.to_string_no_last_nl();
96
97 match regex_syntax::parse(&pat) {
98 Ok(_) => {
99 handle.area().set_print_info(pa, orig_print_info.clone());
100 let buffer = handle.write(pa);
101 *buffer.selections_mut() = orig_selections.clone();
102
103 let ast = regex_syntax::ast::parse::Parser::new()
104 .parse(&text.to_string_no_last_nl())
105 .unwrap();
106
107 crate::tag_from_ast(*NS, &mut text, &ast);
108
109 if !text.is_empty() {
110 self.inc.search(pa, &pat, handle);
111 }
112 }
113 Err(err) => {
114 let regex_syntax::Error::Parse(err) = err else {
115 unreachable!("As far as I can tell, regex_syntax has goofed up");
116 };
117
118 let span = err.span();
119 let id = form::id_of!("regex.error");
120
121 text.insert_tag(*NS, span.start.offset..span.end.offset, id.to_tag(0));
122 }
123 }
124
125 text
126 }
127
128 fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &RwArea) -> Text {
129 let handle = context::current_buffer(pa);
130
131 self.orig = Some((
132 handle.read(pa).selections().clone(),
133 handle.area().get_print_info(pa),
134 ));
135
136 text
137 }
138
139 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
140 if !text.is_empty() {
141 let pat = text.to_string_no_last_nl();
142 if let Err(err) = regex_syntax::parse(&pat) {
143 let regex_syntax::Error::Parse(err) = err else {
144 unreachable!("As far as I can tell, regex_syntax has goofed up");
145 };
146
147 let range = err.span().start.offset..err.span().end.offset;
148 let err = txt!(
149 "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
150 range,
151 &text[range],
152 err.kind()
153 );
154
155 context::error!("{err}")
156 } else {
157 hook::trigger(pa, SearchPerformed(pat));
158 }
159 }
160 }
161
162 fn prompt(&self) -> Text {
163 txt!("{}", self.inc.prompt())
164 }
165}
166
167pub trait IncSearcher: Clone + Send + 'static {
215 fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>);
220
221 fn prompt(&self) -> Text;
225}
226
227#[derive(Clone, Copy)]
231pub struct SearchFwd;
232
233impl IncSearcher for SearchFwd {
234 fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
235 handle.edit_all(pa, |mut c| {
236 if let Some(range) = {
237 c.search(pat).from_caret_excl().next().or_else(|| {
238 context::info!("search wrapped around buffer");
239 c.search(pat).to_caret().next()
240 })
241 } {
242 c.move_to(range)
243 }
244 });
245 }
246
247 fn prompt(&self) -> Text {
248 txt!("[prompt]search")
249 }
250}
251
252#[derive(Clone, Copy)]
256pub struct SearchRev;
257
258impl IncSearcher for SearchRev {
259 fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
260 handle.edit_all(pa, |mut c| {
261 if let Some(range) = {
262 c.search(pat).to_caret().next_back().or_else(|| {
263 context::info!("search wrapped around buffer");
264 c.search(pat).from_caret_excl().next_back()
265 })
266 } {
267 c.move_to(range)
268 }
269 });
270 }
271
272 fn prompt(&self) -> Text {
273 txt!("[prompt]rev search")
274 }
275}
276
277#[derive(Clone, Copy)]
281pub struct ExtendFwd;
282
283impl IncSearcher for ExtendFwd {
284 fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
285 handle.edit_all(pa, |mut c| {
286 if let Some(range) = {
287 c.search(pat).from_caret_excl().next().or_else(|| {
288 context::info!("search wrapped around buffer");
289 c.search(pat).to_caret().next()
290 })
291 } {
292 c.set_anchor_if_needed();
293 c.move_to(range)
294 }
295 });
296 }
297
298 fn prompt(&self) -> Text {
299 txt!("[prompt]search (extend)")
300 }
301}
302
303#[derive(Clone, Copy)]
307pub struct ExtendRev;
308
309impl IncSearcher for ExtendRev {
310 fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
311 handle.edit_all(pa, |mut c| {
312 if let Some(range) = {
313 c.search(pat).to_caret().next_back().or_else(|| {
314 context::info!("search wrapped around buffer");
315 c.search(pat).from_caret_excl().next_back()
316 })
317 } {
318 c.set_anchor_if_needed();
319 c.move_to(range)
320 }
321 });
322 }
323
324 fn prompt(&self) -> Text {
325 txt!("[prompt]rev search (extend)")
326 }
327}