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::{RegexHaystack, 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 bindings() -> mode::Bindings {
47        use duat_core::mode::KeyCode::*;
48
49        if mode::alt_is_reverse() {
50            mode::bindings!(match _ {
51                event!(Char('j') | Down) => txt!("Scroll down"),
52                event!(Char('k') | Up) => txt!("Scroll up"),
53                event!('J') | shift!(Down) => txt!("Scroll to the bottom"),
54                event!('K') | shift!(Up) => txt!("Scroll to the top"),
55                event!('/') => txt!("[mode]Search[] ahead"),
56                alt!('/') => txt!("[mode]Search[] behind"),
57                event!('n') | alt!('n') => txt!("Go to [a]next[],[a]previous[] search match"),
58                event!(Esc) => txt!("[mode]Leave[] pager mode"),
59                event!(':') => txt!("[a]Run commands[] in prompt line"),
60            })
61        } else {
62            mode::bindings!(match _ {
63                event!(Char('j') | Down) => txt!("Scroll down"),
64                event!(Char('k') | Up) => txt!("Scroll up"),
65                event!('J') | shift!(Down) => txt!("Scroll to the bottom"),
66                event!('K') | shift!(Up) => txt!("Scroll to the top"),
67                event!('/') => txt!("[mode]Search[] ahead"),
68                event!('?') => txt!("[mode]Search[] behind"),
69                event!('n' | 'N') => txt!("Go to [a]next[],[a]previous[] search match"),
70                event!(Esc) => txt!("[mode]Leave[] pager mode"),
71                event!(':') => txt!("[a]Run commands[] in prompt line"),
72            })
73        }
74    }
75
76    fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget>) {
77        use duat_core::mode::KeyCode::*;
78
79        match (key, duat_core::mode::alt_is_reverse()) {
80            (event!(Char('j') | Down), _) => handle.scroll_ver(pa, 1),
81            (event!('J') | shift!(Down), _) => handle.scroll_ver(pa, i32::MAX),
82            (event!(Char('k') | Up), _) => handle.scroll_ver(pa, -1),
83            (event!('K') | shift!(Up), _) => handle.scroll_ver(pa, i32::MIN),
84            (event!('/'), _) => _ = mode::set(pa, PagerSearch::new(pa, &handle, true)),
85            (alt!('/'), true) | (event!('?'), false) => {
86                mode::set(pa, PagerSearch::new(pa, &handle, false));
87            }
88            (event!('n'), _) => {
89                let se = SEARCH.lock().unwrap();
90
91                let point = handle.start_points(pa).real;
92
93                let text = handle.read(pa).text();
94                let Some(r) = text.search(&*se).range(point..).next() else {
95                    context::error!("[a]{se}[] was not found");
96                    return;
97                };
98
99                let point = handle.text(pa).point_at_byte(r.start);
100                handle.scroll_to_points(pa, point.to_two_points_after());
101            }
102            (alt!('n'), true) | (event!('N'), false) => {
103                let se = SEARCH.lock().unwrap();
104
105                let point = handle.start_points(pa).real;
106
107                let text = handle.read(pa).text();
108                let Some(r) = text.search(&*se).range(..point).next() else {
109                    context::error!("[a]{se}[] was not found");
110                    return;
111                };
112
113                let point = handle.text(pa).point_at_byte(r.start);
114                handle.scroll_to_points(pa, point.to_two_points_after());
115            }
116            (event!(Esc), _) => _ = mode::reset::<Buffer>(pa),
117            (event!(':'), _) => _ = mode::set(pa, RunCommands::new()),
118            _ => {}
119        }
120    }
121}
122
123impl<W: Widget> Clone for Pager<W> {
124    fn clone(&self) -> Self {
125        Self(PhantomData)
126    }
127}
128
129impl<W: Widget> Default for Pager<W> {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135/// The searcher [`PromptMode`] for a [`Pager`]ed [`Widget`]
136pub struct PagerSearch<W: Widget> {
137    is_fwd: bool,
138    prev: String,
139    orig: PrintInfo,
140    handle: Handle<W>,
141}
142
143impl<W: Widget> PagerSearch<W> {
144    #[allow(clippy::new_ret_no_self)]
145    fn new(pa: &Pass, handle: &Handle<W>, is_fwd: bool) -> Prompt {
146        Prompt::new(Self {
147            is_fwd,
148            prev: String::new(),
149            orig: handle.area().get_print_info(pa),
150            handle: handle.clone(),
151        })
152    }
153}
154
155impl<W: Widget> PromptMode for PagerSearch<W> {
156    type ExitWidget = W;
157
158    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
159        let tagger = *PAGER_TAGGER;
160        text.remove_tags(tagger, ..);
161
162        if text == self.prev.as_str() {
163            return text;
164        } else {
165            let prev = std::mem::replace(&mut self.prev, text.to_string());
166            hook::trigger(pa, SearchUpdated((prev, self.prev.clone())));
167        }
168
169        let (widget, area) = self.handle.write_with_area(pa);
170
171        let mut parts = widget.text_mut().parts();
172
173        match parts.bytes.try_search(text.to_string()) {
174            Ok(matches) => {
175                area.set_print_info(self.orig.clone());
176                parts.tags.remove(*PAGER_TAGGER, ..);
177
178                let ast = regex_syntax::ast::parse::Parser::new()
179                    .parse(&text.to_string())
180                    .unwrap();
181
182                crate::tag_from_ast(*PAGER_TAGGER, &mut text, &ast);
183
184                let id = form::id_of!("pager.search");
185
186                for range in matches {
187                    parts.tags.insert(*PAGER_TAGGER, range, id.to_tag(0));
188                }
189            }
190            Err(err) => {
191                let regex_syntax::Error::Parse(err) = *err else {
192                    unreachable!("As far as I can tell, regex_syntax has goofed up");
193                };
194
195                let span = err.span();
196                let id = form::id_of!("regex.error");
197
198                text.insert_tag(
199                    *PAGER_TAGGER,
200                    span.start.offset..span.end.offset,
201                    id.to_tag(0),
202                );
203            }
204        }
205
206        text
207    }
208
209    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
210        let point = self.handle.start_points(pa).real;
211
212        match self.handle.text(pa).try_search(text.to_string()) {
213            Ok(matches) => {
214                if self.is_fwd {
215                    let Some(range) = matches.clone().range(point..).next() else {
216                        context::error!("[a]{}[] was not found", text.to_string());
217                        return;
218                    };
219
220                    let start = self.handle.text(pa).point_at_byte(range.start);
221                    self.handle
222                        .scroll_to_points(pa, start.to_two_points_after());
223                } else {
224                    let Some(range) = matches.range(..point).next_back() else {
225                        context::error!("[a]{}[] was not found", text.to_string());
226                        return;
227                    };
228
229                    let start = self.handle.text(pa).point_at_byte(range.start);
230                    self.handle
231                        .scroll_to_points(pa, start.to_two_points_after());
232                }
233
234                *SEARCH.lock().unwrap() = text.to_string();
235                hook::trigger(pa, SearchPerformed(text.to_string()));
236            }
237            Err(err) => {
238                let regex_syntax::Error::Parse(err) = *err else {
239                    unreachable!("As far as I can tell, regex_syntax has goofed up");
240                };
241
242                let range = err.span().start.offset..err.span().end.offset;
243                let err = txt!(
244                    "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
245                    range,
246                    text.strs(range).unwrap(),
247                    err.kind()
248                );
249
250                context::error!("{err}")
251            }
252        }
253    }
254
255    fn prompt(&self) -> Text {
256        txt!("[prompt]pager search")
257    }
258
259    fn return_handle(&self) -> Option<Handle<dyn Widget>> {
260        Some(self.handle.clone().to_dyn())
261    }
262}
263
264impl<W: Widget> Clone for PagerSearch<W> {
265    fn clone(&self) -> Self {
266        Self {
267            is_fwd: self.is_fwd,
268            prev: self.prev.clone(),
269            orig: self.orig.clone(),
270            handle: self.handle.clone(),
271        }
272    }
273}