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