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::{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
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 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
104pub 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}