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