duat_base/modes/
inc_search.rs

1//! Utilities for incremental search in Duat
2//!
3//! This specific feature of Duat is kind of split across this crate
4//! and [`duat-core`], since some of the low level features (like
5//! spawning a bazillion [`Cursor`]s) were only possible with access
6//! to private things.
7//!
8//! [`duat-core`]: duat_core
9//! [`Cursor`]: duat_core::mode::Cursor
10use 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
28/// The [`PromptMode`] that makes use of [`IncSearcher`]s
29///
30/// In order to make use of incremental search, you'd do something
31/// like this:
32///
33/// ```rust
34/// # duat_core::doc_duat!(duat);
35/// # use duat_base::modes::{IncSearch, SearchFwd};
36/// use duat::prelude::*;
37///
38/// #[derive(Clone)]
39/// struct Emacs;
40///
41/// impl Mode for Emacs {
42///     type Widget = Buffer;
43///
44///     fn send_key(&mut self, pa: &mut Pass, event: KeyEvent, handle: Handle) {
45///         match event {
46///             ctrl!('s') => _ = mode::set(pa, IncSearch::new(SearchFwd)),
47///             other_keys_oh_god => todo!(),
48///         }
49///     }
50/// }
51/// ```
52pub 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    /// Returns a [`Prompt`] with [`IncSearch<I>`] as its
70    /// [`PromptMode`]
71    #[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
172/// An abstraction trait used to handle incremental search
173///
174/// This trait can be used for various ways of interpreting what
175/// incremental search should do, right now, these are the
176/// implementations of [`IncSearcher`]:
177///
178/// - [`SearchFwd`]: In each cursor, searches forward for the match
179/// - [`SearchRev`]: In each cursor, searches backwards for the match
180/// - [`ExtendFwd`]: In each cursor, extends forward for the match
181/// - [`ExtendRev`]: In each cursor, extends backwards for the match
182///
183/// Here is how you can implement this trait yourself:
184///
185/// ```rust
186/// # duat_core::doc_duat!(duat);
187/// # use duat_base::modes::IncSearcher;
188/// use duat::prelude::*;
189///
190/// #[derive(Clone, Copy)]
191/// struct SearchAround;
192///
193/// impl IncSearcher for SearchAround {
194///     fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>) {
195///         handle.edit_all(pa, |mut c| {
196///             c.set_caret_on_end();
197///             let Some(e_range) = c.search(pat).from_caret().next() else {
198///                 return;
199///             };
200///
201///             c.set_caret_on_start();
202///             let Some(s_range) = c.search(pat).to_caret().next_back() else {
203///                 return;
204///             };
205///
206///             c.move_to(s_range.start..e_range.end)
207///         });
208///     }
209///
210///     fn prompt(&self) -> Text {
211///         txt!("[prompt]search around")
212///     }
213/// }
214/// ```
215///
216/// There are more advanced implementations in the [`duat-kak`] crate
217///
218/// [`duat-kak`]: https://docs.rs/duat-kak
219pub trait IncSearcher: Clone + Send + 'static {
220    /// Performs an incremental search with a `pat`
221    ///
222    /// Using this `pat` inside any searching method is guaranteed not
223    /// to panic.
224    fn search(&mut self, pa: &mut Pass, pat: &str, handle: Handle<Buffer>);
225
226    /// What prompt to show in the [`PromptLine`]
227    ///
228    /// [`PromptLine`]: crate::widgets::PromptLine
229    fn prompt(&self) -> Text;
230}
231
232/// Searches forward on each [`Cursor`]
233///
234/// [`Cursor`]: duat_core::mode::Cursor
235#[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/// Searches backwards on each [`Cursor`]
258///
259/// [`Cursor`]: duat_core::mode::Cursor
260#[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/// Extends forward on each [`Cursor`]
283///
284/// [`Cursor`]: duat_core::mode::Cursor
285#[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/// Extends backwards on each [`Cursor`]
309///
310/// [`Cursor`]: duat_core::mode::Cursor
311#[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}