1use 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
33pub struct Pager<W: Widget = LogBook>(PhantomData<W>);
35
36impl<W: Widget> Pager<W> {
37 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
135pub 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}