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