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