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