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