duat_base/modes/
pager.rs

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