Skip to main content

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