duat_utils/modes/
pager.rs

1use std::{
2    marker::PhantomData,
3    sync::{LazyLock, Mutex},
4};
5
6use duat_core::{prelude::*, text::Searcher};
7
8use crate::{
9    hooks::{SearchPerformed, SearchUpdated},
10    modes::{Prompt, PromptMode},
11};
12
13static SEARCH: Mutex<String> = Mutex::new(String::new());
14static PAGER_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
15
16/// A simple mode, meant for scrolling and searching through [`Text`]
17pub struct Pager<W: Widget<U>, U: Ui>(PhantomData<(W, U)>);
18
19impl<W: Widget<U>, U: Ui> Pager<W, U> {
20    /// Returns a new [`Pager`]
21    pub fn new() -> Self {
22        Self(PhantomData)
23    }
24}
25
26impl<W: Widget<U>, U: Ui> Mode<U> for Pager<W, U> {
27    type Widget = W;
28
29    fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget, U>) {
30        use KeyCode::*;
31        match (key, duat_core::mode::alt_is_reverse()) {
32            (key!(Char('j') | Down), _) => {
33                handle.scroll_ver(pa, 1);
34            }
35            (key!(Char('k') | Up), _) => {
36                handle.scroll_ver(pa, -1);
37            }
38            (key!(Char('/')), _) => {
39                mode::set::<U>(PagerSearch::new(&handle, true));
40            }
41            (key!(Char('/'), KeyMod::ALT), true) | (key!(Char('?')), false) => {
42                mode::set::<U>(PagerSearch::new(&handle, false));
43            }
44            (key!(Char('n')), _) => {
45                let se = SEARCH.lock().unwrap();
46
47                let (point, _) = handle.start_points(pa);
48
49                let Some([point, _]) =
50                    handle.write_text(pa, |t| t.search_fwd(&*se, point..).unwrap().next())
51                else {
52                    context::error!("[a]{se}[] was not found");
53                    return;
54                };
55
56                handle.scroll_to_points(pa, point);
57            }
58            (key!(Char('n'), KeyMod::ALT), true) | (key!(Char('N')), false) => {
59                let se = SEARCH.lock().unwrap();
60
61                let (point, _) = handle.start_points(pa);
62
63                let Some([point, _]) =
64                    handle.write_text(pa, |t| t.search_rev(&*se, point..).unwrap().next())
65                else {
66                    context::error!("[a]{se}[] was not found");
67                    return;
68                };
69
70                handle.scroll_to_points(pa, point);
71            }
72            (key!(Esc), _) => mode::reset::<File<U>, U>(),
73            _ => {}
74        }
75    }
76}
77
78impl<W: Widget<U>, U: Ui> Clone for Pager<W, U> {
79    fn clone(&self) -> Self {
80        Self(PhantomData)
81    }
82}
83
84impl<W: Widget<U>, U: Ui> Default for Pager<W, U> {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90/// The searcher [`PromptMode`] for a [`Pager`]ed [`Widget`]
91pub struct PagerSearch<W: Widget<U>, U: Ui> {
92    is_fwd: bool,
93    prev: String,
94    orig: <U::Area as RawArea>::PrintInfo,
95    handle: Handle<W, U>,
96}
97
98impl<W: Widget<U>, U: Ui> PagerSearch<W, U> {
99    fn new(handle: &Handle<W, U>, is_fwd: bool) -> Prompt<Self, U> {
100        Prompt::new(Self {
101            is_fwd,
102            prev: String::new(),
103            orig: handle.area().print_info(),
104            handle: handle.clone(),
105        })
106    }
107}
108
109impl<W: Widget<U>, U: Ui> PromptMode<U> for PagerSearch<W, U> {
110    type ExitWidget = W;
111
112    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &<U as Ui>::Area) -> Text {
113        let tagger = *PAGER_TAGGER;
114        text.remove_tags(tagger, ..);
115
116        if text == self.prev.as_str() {
117            return text;
118        } else {
119            let prev = std::mem::replace(&mut self.prev, text.to_string());
120            hook::queue(SearchUpdated((prev, self.prev.clone())));
121        }
122
123        match Searcher::new(text.to_string()) {
124            Ok(mut searcher) => {
125                self.handle.write(pa, |widget, area| {
126                    area.set_print_info(self.orig.clone());
127                    widget.text_mut().remove_tags(*PAGER_TAGGER, ..);
128                });
129
130                let ast = regex_syntax::ast::parse::Parser::new()
131                    .parse(&text.to_string())
132                    .unwrap();
133
134                crate::tag_from_ast(*PAGER_TAGGER, &mut text, &ast);
135
136                self.handle.write(pa, |widget, _| {
137                    let (bytes, mut tags) = widget.text_mut().bytes_and_tags();
138
139                    let id = form::id_of!("pager.search");
140
141                    for [start, end] in searcher.search_fwd(bytes, ..) {
142                        tags.insert(*PAGER_TAGGER, start..end, id.to_tag(0));
143                    }
144                });
145            }
146            Err(err) => {
147                let regex_syntax::Error::Parse(err) = *err else {
148                    unreachable!("As far as I can tell, regex_syntax has goofed up");
149                };
150
151                let span = err.span();
152                let id = form::id_of!("regex.error");
153
154                text.insert_tag(
155                    *PAGER_TAGGER,
156                    span.start.offset..span.end.offset,
157                    id.to_tag(0),
158                );
159            }
160        }
161
162        text
163    }
164
165    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &<U as Ui>::Area) {
166        match Searcher::new(text.to_string()) {
167            Ok(mut se) => {
168                let (point, _) = self.handle.start_points(pa);
169                if self.is_fwd {
170                    let Some([point, _]) = self
171                        .handle
172                        .write_text(pa, |t| se.search_fwd(t, point..).next())
173                    else {
174                        context::error!("[a]{}[] was not found", text.to_string());
175                        return;
176                    };
177
178                    self.handle.scroll_to_points(pa, point);
179                } else {
180                    let Some([point, _]) = self
181                        .handle
182                        .write_text(pa, |t| se.search_rev(t, ..point).next())
183                    else {
184                        context::error!("[a]{}[] was not found", text.to_string());
185                        return;
186                    };
187
188                    self.handle.scroll_to_points(pa, point);
189                }
190
191                *SEARCH.lock().unwrap() = text.to_string();
192                hook::queue(SearchPerformed(text.to_string()));
193            }
194            Err(err) => {
195                let regex_syntax::Error::Parse(err) = *err else {
196                    unreachable!("As far as I can tell, regex_syntax has goofed up");
197                };
198
199                let range = err.span().start.offset..err.span().end.offset;
200                let err = txt!(
201                    "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
202                    range,
203                    text.strs(range),
204                    err.kind()
205                );
206
207                context::error!(target: "pager search", "{err}")
208            }
209        }
210    }
211
212    fn prompt(&self) -> Text {
213        txt!("[prompt]pager search").build()
214    }
215
216    fn return_handle(&self) -> Option<Handle<Self::ExitWidget, U>> {
217        Some(self.handle.clone())
218    }
219}
220
221impl<W: Widget<U>, U: Ui> Clone for PagerSearch<W, U> {
222    fn clone(&self) -> Self {
223        Self {
224            is_fwd: self.is_fwd,
225            prev: self.prev.clone(),
226            orig: self.orig.clone(),
227            handle: self.handle.clone(),
228        }
229    }
230}