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