1use std::{
121 ops::Range,
122 sync::{LazyLock, Mutex},
123};
124
125use duat::{
126 mode::{KeyCode::*, KeyMod},
127 prelude::*,
128};
129
130static TAGGER: LazyLock<Tagger> = Tagger::new_static();
131static CUR_TAGGER: LazyLock<Tagger> = Tagger::new_static();
132static CLOAK_TAGGER: LazyLock<Tagger> = Tagger::new_static();
133static LAST: Mutex<String> = Mutex::new(String::new());
134
135#[derive(Clone)]
137pub struct Sneak {
138 step: Step,
139 len: usize,
140 prev_key: KeyEvent,
141 next_key: KeyEvent,
142 min_for_labels: usize,
143}
144
145impl Sneak {
146 pub fn new() -> Self {
148 Self {
149 step: Step::Start,
150 len: 2,
151 next_key: KeyCode::Char('n').into(),
152 prev_key: if mode::alt_is_reverse() {
153 KeyEvent::new(KeyCode::Char('n'), KeyMod::ALT)
154 } else {
155 Char('N').into()
156 },
157 min_for_labels: usize::MAX,
158 }
159 }
160
161 pub fn select_keys(self, prev: char, next: char) -> Self {
172 Self {
173 prev_key: Char(prev).into(),
174 next_key: Char(next).into(),
175 ..self
176 }
177 }
178
179 #[track_caller]
181 pub fn with_len(self, len: usize) -> Self {
182 assert!(len >= 1, "Can't match on 0 characters");
183 Self { len, ..self }
184 }
185
186 pub fn min_for_labels(self, min_for_labels: usize) -> Self {
199 Self { min_for_labels, ..self }
200 }
201}
202
203impl Plugin for Sneak {
204 fn plug(self, _: &Plugins) {
205 mode::map::<mode::User>("s", self);
206
207 form::set_weak("sneak.match", "default.info");
208 form::set_weak("sneak.label", "accent.info");
209 form::set_weak("sneak.current", Form::underlined());
210 }
211}
212
213impl Mode for Sneak {
214 type Widget = Buffer;
215
216 fn bindings() -> mode::Bindings {
217 mode::bindings!(match _ {
218 event!(Char(..)) => txt!("Filter by [key.char]{{char}}"),
219 })
220 }
221
222 fn send_key(&mut self, pa: &mut Pass, key: mode::KeyEvent, handle: Handle) {
223 match &mut self.step {
224 Step::Start => {
225 let (pat, finished_filtering) = if let event!(Char(char)) = key {
226 (char.to_string(), self.len == 1)
227 } else {
228 let last = LAST.lock().unwrap();
229
230 if last.is_empty() {
231 context::error!("mode hasn't been set to [a]Sneak[] yet");
232 mode::reset::<Buffer>(pa);
233 return;
234 } else {
235 (last.clone(), true)
236 }
237 };
238
239 let regex = format!("{pat}[^\n]{{{}}}", self.len - pat.chars().count());
240 let (matches, cur) = hi_matches(pa, ®ex, &handle);
241
242 let Some(cur) = cur else {
243 context::error!("No matches found for [a]{pat}");
244 mode::reset::<Buffer>(pa);
245 return;
246 };
247
248 self.step = if finished_filtering {
249 if matches.len() == 1 {
251 let range = matches[0].clone();
252 handle.edit_main(pa, |mut c| c.move_to(range));
253
254 mode::reset::<Buffer>(pa);
255
256 Step::MatchedMove(pat, matches, cur)
257 } else if matches.len() >= self.min_for_labels {
258 hi_labels(pa, &handle, &matches);
259
260 Step::MatchedLabels(pat, matches)
261 } else {
262 hi_cur(pa, &handle, matches[cur].clone(), matches[cur].clone());
263
264 Step::MatchedMove(pat, matches, cur)
265 }
266 } else {
267 Step::Filter(pat)
268 }
269 }
270 Step::Filter(pat) => {
271 handle.text_mut(pa).remove_tags(*TAGGER, ..);
272
273 let (regex, finished_filtering) = if let event!(Char(char)) = key {
274 pat.push(char);
275
276 let regex = format!("{pat}[^\n]{{{}}}", self.len - pat.chars().count());
277 (regex, pat.chars().count() >= self.len)
278 } else {
279 (pat.clone(), true)
280 };
281
282 let (matches, cur) = hi_matches(pa, ®ex, &handle);
283
284 let Some(cur) = cur else {
285 context::error!("No matches found for [a]{pat}");
286 mode::reset::<Buffer>(pa);
287 return;
288 };
289
290 hi_cur(pa, &handle, matches[cur].clone(), matches[cur].clone());
291
292 if finished_filtering {
293 self.step = if matches.len() == 1 {
295 let range = matches[0].clone();
296 handle.edit_main(pa, |mut c| c.move_to(range));
297
298 mode::reset::<Buffer>(pa);
299
300 Step::MatchedMove(pat.clone(), matches, cur)
301 } else if matches.len() >= self.min_for_labels {
302 hi_labels(pa, &handle, &matches);
303
304 Step::MatchedLabels(pat.clone(), matches)
305 } else {
306 hi_cur(pa, &handle, matches[cur].clone(), matches[cur].clone());
307
308 Step::MatchedMove(pat.clone(), matches, cur)
309 };
310 }
311 }
312 Step::MatchedMove(_, matches, cur) => {
313 let prev = *cur;
314 let last = matches.len() - 1;
315
316 if key == self.next_key {
317 *cur = if *cur == last { 0 } else { *cur + 1 };
318 hi_cur(pa, &handle, matches[*cur].clone(), matches[prev].clone());
319 } else if key == self.prev_key {
320 *cur = if *cur == 0 { last } else { *cur - 1 };
321 hi_cur(pa, &handle, matches[*cur].clone(), matches[prev].clone());
322 } else {
323 let range = matches[*cur].clone();
324 handle.edit_main(pa, |mut c| c.move_to(range));
325
326 mode::reset::<Buffer>(pa);
327 }
328 }
329 Step::MatchedLabels(_, matches) => {
330 handle.text_mut(pa).remove_tags(*TAGGER, ..);
331
332 let filtered_label = if let event!(Char(char)) = key
333 && iter_labels(matches.len()).any(|label| char == label)
334 {
335 char
336 } else {
337 if let event!(Char(char)) = key {
338 context::error!("[a]{char}[] is not a valid label");
339 } else {
340 context::error!("[a]{key.code:?}[] is not a valid label");
341 }
342 mode::reset::<Buffer>(pa);
343 return;
344 };
345
346 let mut iter = iter_labels(matches.len());
347 matches.retain(|_| iter.next() == Some(filtered_label));
348
349 if matches.len() == 1 {
350 let range = matches[0].clone();
351 handle.edit_main(pa, |mut c| c.move_to(range));
352
353 mode::reset::<Buffer>(pa);
354 } else {
355 hi_labels(pa, &handle, matches);
356 }
357 }
358 }
359 }
360
361 fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
362 let id = form::id_of!("cloak");
363 handle
364 .text_mut(pa)
365 .insert_tag(*CLOAK_TAGGER, .., id.to_tag(101));
366 }
367
368 fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
369 use Step::*;
370 if let Filter(pat) | MatchedMove(pat, ..) | MatchedLabels(pat, _) = &self.step {
371 *LAST.lock().unwrap() = pat.clone();
372 }
373
374 handle
375 .text_mut(pa)
376 .remove_tags([*TAGGER, *CUR_TAGGER, *CLOAK_TAGGER], ..)
377 }
378}
379
380fn hi_labels(pa: &mut Pass, handle: &Handle, matches: &Vec<Range<usize>>) {
381 let mut text = handle.text_mut(pa);
382
383 text.remove_tags([*TAGGER, *CUR_TAGGER], ..);
384
385 for (label, range) in iter_labels(matches.len()).zip(matches) {
386 let ghost = Ghost::new(txt!("[sneak.label:102]{label}"));
387 text.insert_tag(*TAGGER, range.start, ghost);
388
389 let len = text.char_at(range.start).map(|c| c.len_utf8()).unwrap_or(1);
390 text.insert_tag(*TAGGER, range.start..range.start + len, Conceal);
391 }
392}
393
394fn hi_matches(pa: &mut Pass, pat: &str, handle: &Handle) -> (Vec<Range<usize>>, Option<usize>) {
395 let (buffer, area) = handle.write_with_area(pa);
396
397 let start = area.start_points(buffer.text(), buffer.opts).real;
398 let end = area.end_points(buffer.text(), buffer.opts).real;
399 let caret = buffer.selections().main().caret().byte();
400
401 let mut parts = buffer.text_mut().parts();
402
403 let matches: Vec<_> = parts.bytes.search(pat).range(start..end).collect();
404
405 let id = form::id_of!("sneak.match");
406
407 let tagger = *TAGGER;
408 let mut next = None;
409 for (i, range) in matches.iter().enumerate() {
410 if range.start > caret && next.is_none() {
411 next = Some(i);
412 }
413 parts.tags.insert(tagger, range.clone(), id.to_tag(102));
414 }
415
416 let last = matches.len().checked_sub(1);
417 (matches, next.or(last))
418}
419
420fn hi_cur(pa: &mut Pass, handle: &Handle, cur: Range<usize>, prev: Range<usize>) {
421 let cur_id = form::id_of!("sneak.current");
422
423 let mut text = handle.text_mut(pa);
424 text.remove_tags(*CUR_TAGGER, prev.start);
425 text.insert_tag(*CUR_TAGGER, cur, cur_id.to_tag(103));
426}
427
428fn iter_labels(total: usize) -> impl Iterator<Item = char> {
429 const LETTERS: &str = "abcdefghijklmnopqrstuvwxyz";
430
431 let multiple = total / LETTERS.len();
432
433 let singular = LETTERS.chars().skip(multiple);
434
435 singular
436 .chain(
437 LETTERS
438 .chars()
439 .take(multiple)
440 .flat_map(|c| std::iter::repeat_n(c, 26)),
441 )
442 .take(total)
443}
444
445#[derive(Clone)]
446enum Step {
447 Start,
448 Filter(String),
449 MatchedMove(String, Vec<Range<usize>>, usize),
450 MatchedLabels(String, Vec<Range<usize>>),
451}
452
453impl Default for Sneak {
454 fn default() -> Self {
455 Self::new()
456 }
457}