duat_base/modes/
prompt.rs

1//! Multi modal for controlling the [`Prompt`] widget
2//!
3//! This mode's purpose is to do actions based on a [`PromptMode`]
4//! implementor. `PromptMode` implementors take in the [`Text`] of the
5//! [`Prompt`], and output some transformation to said `Text` (e.g.
6//! formatting), while also doing actions given the global access
7//! through the [`Pass`].
8//!
9//! Examples of [`PromptMode`]s are [`RunCommands`] and [`IncSearch`].
10//! The former is used to run Duat's commands, while the latter
11//! searches based on an input regex.
12//!
13//! `IncSearch` itself is _also_ multimodal, in an even more niche
14//! sense. It takes in an [`IncSearcher`] implementor, and searches
15//! through the [`Buffer`] according to its rules. Examples of this
16//! are [`SearchFwd`] and [`SearchRev`], which take in the regex and
17//! search in their respective directions. There are also more
18//! "advanced" `IncSearcher`s, like the ones in the `duatmode` crate,
19//! which can split a [`Selection`] by a regex, or keeps `Selections`s
20//! that match, that sort of thing.
21//!
22//! [`SearchFwd`]: super::SearchFwd
23//! [`SearchRev`]: super::SearchRev
24//! [`Selection`]: duat_core::mode::Selection
25//! [`IncSearch`]: crate::modes::IncSearch
26//! [`IncSearcher`]: crate::modes::IncSearcher
27use std::{
28    any::TypeId,
29    io::Write,
30    sync::{Arc, LazyLock, Mutex, Once},
31};
32
33use duat_core::{
34    buffer::Buffer,
35    cmd,
36    context::{self, Handle},
37    data::Pass,
38    form,
39    mode::{self, KeyEvent, event, shift},
40    text::{Ghost, Tagger, Text, txt},
41    ui::{RwArea, Widget},
42};
43
44use crate::widgets::{CommandsCompletions, Completions, PromptLine};
45
46static HISTORY: Mutex<Vec<(TypeId, Vec<String>)>> = Mutex::new(Vec::new());
47static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
48static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
49static PREVIEW_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
50
51/// A [`Mode`] for the [`PromptLine`]
52///
53/// This mode abstracts over what the inner [`PromptMode`] actually
54/// does, by letting them focus on just updating the [`Text`] and
55/// acting on user input, instead of having to worry about which keys
56/// do what, and when to update.
57///
58/// There are currently three [`PromptMode`]s:
59///
60/// - [`RunCommands`] is just your regular command runner, it can also
61///   detect if your [`Parameter`]s are correct and show that in real
62///   time.
63/// - [`PipeSelections`] pipes each [`Selection`]'s selection in the
64///   current [`Buffer`] to an external application, replacing each
65///   selection with the returned value.
66/// - [`IncSearch`] has a further inner abstraction, [`IncSearcher`],
67///   which lets you abstract over what the incremental search will
68///   actually do. I.c. will it search for the next ocurrence, split
69///   selections by matches, things of the sort.
70///
71/// [`Parameter`]: cmd::Parameter
72/// [`Selection`]: mode::Selection
73/// [`Mode`]: duat_core::mode::Mode
74/// [`IncSearch`]: crate::modes::IncSearch
75/// [`IncSearcher`]: crate::modes::IncSearcher
76pub struct Prompt {
77    mode: Box<dyn PromptMode>,
78    starting_text: String,
79    ty: TypeId,
80    clone_fn: Arc<Mutex<ModeCloneFn>>,
81    reset_fn: fn(pa: &mut Pass),
82    history_index: Option<usize>,
83}
84
85impl Prompt {
86    /// Returns a new [`Prompt`] from this [`PromptMode`]
87    ///
88    /// For convenience, you should make it so `new` methods in
89    /// [`PromptMode`] implementors return a [`Prompt<Self>`],
90    /// rather than the [`PromptMode`] itself.
91    pub fn new<M: PromptMode + Clone>(mode: M) -> Self {
92        let clone_fn = Arc::new(Mutex::new({
93            let mode = mode.clone();
94            move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
95        }));
96
97        Self {
98            mode: Box::new(mode),
99            starting_text: String::new(),
100            ty: TypeId::of::<M>(),
101            clone_fn,
102            reset_fn: |pa| _ = mode::reset::<M::ExitWidget>(pa),
103            history_index: None,
104        }
105    }
106
107    /// Returns a new [`Prompt`] with some initial text
108    ///
109    /// This is useful if you wish to open this [`Mode`] with some
110    /// text already in it.
111    ///
112    /// [`Mode`]: mode::Mode
113    pub fn new_with<M: PromptMode + Clone>(mode: M, initial: impl ToString) -> Self {
114        let clone_fn = Arc::new(Mutex::new({
115            let mode = mode.clone();
116            move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
117        }));
118
119        Self {
120            mode: Box::new(mode),
121            starting_text: initial.to_string(),
122            ty: TypeId::of::<M>(),
123            clone_fn,
124            reset_fn: |pa| _ = mode::reset::<M::ExitWidget>(pa),
125            history_index: None,
126        }
127    }
128
129    /// Shows the preview [`Ghost`]
130    fn show_preview(&mut self, pa: &mut Pass, handle: Handle<PromptLine>) {
131        let history = HISTORY.lock().unwrap();
132        if handle.text(pa).is_empty()
133            && let Some((_, ty_history)) = history.iter().find(|(ty, _)| *ty == self.ty)
134        {
135            handle.text_mut(pa).insert_tag_after(
136                *PREVIEW_TAGGER,
137                0,
138                Ghost::new(txt!("[prompt.preview]{}", ty_history.last().unwrap())),
139            );
140        }
141    }
142}
143
144impl mode::Mode for Prompt {
145    type Widget = PromptLine;
146
147    fn bindings() -> mode::Bindings {
148        use mode::KeyCode::*;
149
150        mode::bindings!(match _ {
151            event!(Char(..)) => txt!("Insert the character"),
152            event!(Left | Right) => txt!("Move cursor"),
153            event!(Down | Up) => txt!("Move through command history"),
154            event!(Backspace | Delete) => txt!("Remove character or selection"),
155            event!(Enter) => txt!("Run command and [mode]leave"),
156            event!(Esc) => txt!("[mode]Leave[] without running command"),
157        })
158    }
159
160    fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget>) {
161        use duat_core::mode::KeyCode::*;
162
163        let ty_eq = |&&(ty, _): &&(TypeId, _)| ty == self.ty;
164
165        let mut update = |pa: &mut Pass| {
166            let text = std::mem::take(&mut handle.write(pa).text);
167            let text = self.mode.update(pa, text, handle.area());
168            handle.write(pa).text = text;
169        };
170
171        let reset = |pa: &mut Pass, prompt: &mut Self| {
172            if let Some(ret_handle) = prompt.mode.return_handle() {
173                mode::reset_to(pa, &ret_handle);
174            } else {
175                (prompt.reset_fn)(pa);
176            }
177        };
178
179        handle.text_mut(pa).remove_tags(*PREVIEW_TAGGER, ..);
180
181        match key {
182            event!(Char(char)) => {
183                handle.edit_main(pa, |mut c| {
184                    c.insert(char);
185                    c.move_hor(1);
186                });
187                update(pa);
188            }
189
190            event!(Backspace) => {
191                if handle.read(pa).text().is_empty() {
192                    handle.write(pa).text_mut().selections_mut().clear();
193
194                    update(pa);
195
196                    if let Some(ret_handle) = self.mode.return_handle() {
197                        mode::reset_to(pa, &ret_handle);
198                    } else {
199                        (self.reset_fn)(pa);
200                    }
201                } else {
202                    handle.edit_main(pa, |mut c| {
203                        c.move_hor(-1);
204                        c.set_anchor_if_needed();
205                        c.replace("");
206                        c.unset_anchor();
207                    });
208                    update(pa);
209                }
210            }
211            event!(Delete) => {
212                handle.edit_main(pa, |mut c| {
213                    c.set_anchor_if_needed();
214                    c.replace("");
215                });
216                update(pa);
217            }
218
219            event!(Left) => {
220                handle.edit_main(pa, |mut c| c.move_hor(-1));
221                update(pa);
222            }
223            event!(Right) => {
224                handle.edit_main(pa, |mut c| c.move_hor(1));
225                update(pa);
226            }
227            event!(Up) => {
228                let history = HISTORY.lock().unwrap();
229                let Some((_, ty_history)) = history.iter().find(ty_eq) else {
230                    return;
231                };
232
233                let index = if let Some(index) = &mut self.history_index {
234                    *index = index.saturating_sub(1);
235                    *index
236                } else {
237                    self.history_index = Some(ty_history.len() - 1);
238                    ty_history.len() - 1
239                };
240
241                handle.edit_main(pa, |mut c| {
242                    c.move_to(..);
243                    c.replace(ty_history[index].clone());
244                    c.unset_anchor();
245                });
246
247                update(pa);
248            }
249            event!(Down) => {
250                let history = HISTORY.lock().unwrap();
251                let Some((_, ty_history)) = history.iter().find(ty_eq) else {
252                    return;
253                };
254
255                if let Some(index) = &mut self.history_index {
256                    if *index + 1 < ty_history.len() {
257                        *index = (*index + 1).min(ty_history.len() - 1);
258
259                        handle.edit_main(pa, |mut c| {
260                            c.move_to(..);
261                            c.replace(ty_history[*index].clone());
262                            c.unset_anchor();
263                        })
264                    } else {
265                        self.history_index = None;
266                        handle.edit_main(pa, |mut c| {
267                            c.move_to(..);
268                            c.replace("");
269                            c.unset_anchor();
270                        })
271                    }
272                };
273
274                update(pa);
275            }
276
277            event!(Tab) => {
278                Completions::scroll(pa, 1);
279                update(pa);
280            }
281            shift!(BackTab) => {
282                Completions::scroll(pa, -1);
283                update(pa);
284            }
285
286            event!(Esc) => {
287                handle.edit_main(pa, |mut c| {
288                    c.move_to(..);
289                    c.replace("");
290                });
291                handle.write(pa).text_mut().selections_mut().clear();
292
293                update(pa);
294                reset(pa, self);
295            }
296            event!(Enter) => {
297                handle.write(pa).text_mut().selections_mut().clear();
298
299                if handle.text(pa).is_empty() {
300                    let history = HISTORY.lock().unwrap();
301                    if let Some((_, ty_history)) = history.iter().find(ty_eq) {
302                        handle.edit_main(pa, |mut c| {
303                            c.move_to(..);
304                            c.replace(ty_history.last().unwrap());
305                        });
306                    }
307                }
308
309                update(pa);
310                reset(pa, self);
311            }
312            _ => {}
313        }
314
315        self.mode.post_update(pa, &handle);
316        self.show_preview(pa, handle);
317    }
318
319    fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
320        let text = {
321            let pl = handle.write(pa);
322            pl.text = Text::with_default_main_selection();
323            pl.text_mut().replace_range(0..0, &self.starting_text);
324
325            let tag = Ghost::new(match pl.prompt_of_id(self.ty) {
326                Some(text) => txt!("{text}[prompt.colon]:"),
327                None => txt!("{}[prompt.colon]:", self.mode.prompt()),
328            });
329            pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
330
331            std::mem::take(&mut pl.text)
332        };
333
334        let text = self.mode.on_switch(pa, text, handle.area());
335
336        handle.write(pa).text = text;
337
338        self.show_preview(pa, handle);
339    }
340
341    fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
342        let text = std::mem::take(&mut handle.write(pa).text);
343        if !text.is_empty() {
344            let mut history = HISTORY.lock().unwrap();
345            if let Some((_, ty_history)) = history.iter_mut().find(|(ty, _)| *ty == self.ty) {
346                if ty_history.last().is_none_or(|last| last != text.bytes()) {
347                    ty_history.push(text.to_string());
348                }
349            } else {
350                history.push((self.ty, vec![text.to_string()]));
351            }
352        }
353
354        self.mode.before_exit(pa, text, handle.area());
355    }
356}
357
358impl Clone for Prompt {
359    fn clone(&self) -> Self {
360        Self {
361            mode: self.clone_fn.lock().unwrap()(),
362            starting_text: self.starting_text.clone(),
363            ty: self.ty,
364            clone_fn: self.clone_fn.clone(),
365            reset_fn: self.reset_fn,
366            history_index: None,
367        }
368    }
369}
370
371/// A mode to control the [`Prompt`]
372///
373/// Through the [`Pass`], one can act on the entirety of Duat's shared
374/// state:
375///
376/// ```rust
377/// # duat_core::doc_duat!(duat);
378/// # use duat_base::modes::PromptMode;
379/// use duat::prelude::*;
380///
381/// #[derive(Default, Clone)]
382/// struct RealTimeSwitch {
383///     initial: Option<String>,
384///     current: Option<String>,
385///     name_was_correct: bool,
386/// };
387///
388/// impl PromptMode for RealTimeSwitch {
389///     type ExitWidget = Buffer;
390///
391///     fn update(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) -> Text {
392///         let name = text.to_string();
393///
394///         self.name_was_correct = if name != *self.current.as_ref().unwrap() {
395///             if cmd::buffer(pa, &name).is_ok() {
396///                 self.current = Some(name);
397///                 true
398///             } else {
399///                 false
400///             }
401///         } else {
402///             true
403///         };
404///
405///         text
406///     }
407///
408///     fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) -> Text {
409///         self.initial = Some(context::current_buffer(pa).read(pa).name());
410///         self.current = self.initial.clone();
411///
412///         text
413///     }
414///
415///     fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &ui::RwArea) {
416///         if !self.name_was_correct {
417///             cmd::buffer(pa, self.initial.take().unwrap());
418///         }
419///     }
420///
421///     fn prompt(&self) -> Text {
422///         txt!("[prompt]switch to")
423///     }
424/// }
425/// ```
426///
427/// The [`PromptMode`] above will switch to the buffer with the same
428/// name as the one in the [`PromptLine`], returning to the initial
429/// buffer if the match failed.
430#[allow(unused_variables)]
431pub trait PromptMode: Send + 'static {
432    /// What [`Widget`] to exit to, upon pressing enter, esc, or
433    /// backspace in an empty [`PromptLine`]
434    ///
435    /// Usually, this would be [`Buffer`]
436    type ExitWidget: Widget
437    where
438        Self: Sized;
439
440    /// Updates the [`PromptLine`] and [`Text`] of the [`Prompt`]
441    ///
442    /// This function is triggered every time the user presses a key
443    /// in the [`Prompt`] mode.
444    fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
445
446    /// What to do when switchin onto this [`PromptMode`]
447    ///
448    /// The initial [`Text`] is always empty, except for the [prompt]
449    /// [`Ghost`] at the beginning of the line.
450    ///
451    /// [prompt]: PromptMode::prompt
452    fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
453        text
454    }
455
456    /// What to do before exiting the [`PromptMode`]
457    ///
458    /// This usually involves some sor of "commitment" to the result,
459    /// e.g., [`RunCommands`] executes the call, [`IncSearch`]
460    /// finishes the search, etc.
461    ///
462    /// [`IncSearch`]: crate::modes::IncSearch
463    fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
464
465    /// A post update hook to be called on the [`Handle`] itself
466    ///
467    /// One useful thing that you can do on this function is a call to
468    /// [`CompletionsBuilder::open`], which doesn't work on
469    /// [`PromptMode::update`] because the [`Text`] of the
470    /// [`PromptLine`] is taken.
471    ///
472    /// [`CompletionsBuilder::open`]: crate::widgets::CompletionsBuilder::open
473    fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {}
474
475    /// What text should be at the beginning of the [`PromptLine`], as
476    /// a [`Ghost`]
477    fn prompt(&self) -> Text;
478
479    /// An optional returning [`Handle`] for the [`ExitWidget`]
480    ///
481    /// [`ExitWidget`]: PromptMode::ExitWidget
482    fn return_handle(&self) -> Option<Handle<dyn Widget>> {
483        None
484    }
485}
486
487/// Runs Duat commands, with syntax highlighting for correct
488/// [`Parameter`]s
489///
490/// [`Parameter`]: duat_core::cmd::Parameter
491#[derive(Default, Clone)]
492pub struct RunCommands(Option<Completion>);
493
494impl RunCommands {
495    /// Crates a new [`RunCommands`]
496    #[allow(clippy::new_ret_no_self)]
497    pub fn new() -> Prompt {
498        Self::call_once();
499        Prompt::new(Self(None))
500    }
501
502    /// Opens a [`RunCommands`] with some initial text
503    pub fn new_with(initial: impl ToString) -> Prompt {
504        Self::call_once();
505        Prompt::new_with(Self(None), initial)
506    }
507
508    fn call_once() {
509        static ONCE: Once = Once::new();
510        ONCE.call_once(|| {
511            form::set_weak("caller.info", "accent.info");
512            form::set_weak("caller.error", "accent.error");
513            form::set_weak("param.info", "default.info");
514            form::set_weak("param.error", "default.error");
515        });
516    }
517}
518
519impl PromptMode for RunCommands {
520    type ExitWidget = Buffer;
521
522    fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
523        text.remove_tags(*TAGGER, ..);
524
525        let command = text.to_string();
526        let caller = command.split_whitespace().next();
527        if let Some(caller) = caller {
528            if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
529                let id = form::id_of!("caller.info");
530                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
531
532                let default_id = form::id_of!("param.info");
533                for (range, id) in ok_ranges {
534                    text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
535                }
536                if let Some((range, _)) = err_range {
537                    let id = form::id_of!("param.error");
538                    text.insert_tag(*TAGGER, range, id.to_tag(0));
539                }
540            } else {
541                let id = form::id_of!("caller.error");
542                text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
543            }
544        }
545
546        text
547    }
548
549    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
550        let call = text.to_string();
551        if !call.is_empty() {
552            _ = cmd::call_notify(pa, call);
553        }
554    }
555
556    fn post_update(&mut self, pa: &mut Pass, handle: &Handle<PromptLine>) {
557        let text = handle.text(pa);
558        let Some(main) = text.get_main_sel() else {
559            Completions::close(pa);
560            return;
561        };
562
563        let is_parameter = text
564            .chars_rev(..main.caret())
565            .unwrap()
566            .any(|(_, char)| char.is_whitespace());
567
568        let new_completion = if is_parameter {
569            let call = &text.strs(..main.caret()).unwrap().to_string();
570            let Some(parameters) = cmd::last_parsed_parameters(pa, call) else {
571                self.0 = None;
572                Completions::close(pa);
573                return;
574            };
575
576            Completion::Parameters(parameters)
577        } else {
578            Completion::Caller
579        };
580
581        if self.0.as_ref() != Some(&new_completion) {
582            match &new_completion {
583                Completion::Caller => Completions::builder()
584                    .with_provider(CommandsCompletions::new(pa))
585                    .open(pa),
586                Completion::Parameters(params) => Completions::open_for(pa, params),
587            }
588        }
589
590        self.0 = Some(new_completion)
591    }
592
593    fn prompt(&self) -> Text {
594        Text::default()
595    }
596}
597
598/// Pipes the selections of a [`Buffer`] through an external command
599///
600/// This can be useful if you, for example, don't have access to a
601/// formatter, but want to format text, so you pass it to
602/// [`PipeSelections`] with `fold` as the command, or things of the
603/// sort.
604#[derive(Clone, Copy)]
605pub struct PipeSelections;
606
607impl PipeSelections {
608    /// Returns a [`Prompt`] with [`PipeSelections`] as its
609    /// [`PromptMode`]
610    #[allow(clippy::new_ret_no_self)]
611    pub fn new() -> Prompt {
612        Prompt::new(Self)
613    }
614}
615
616impl PromptMode for PipeSelections {
617    type ExitWidget = Buffer;
618
619    fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
620        fn is_in_path(program: &str) -> bool {
621            if let Ok(path) = std::env::var("PATH") {
622                for p in path.split(":") {
623                    let p_str = format!("{p}/{program}");
624                    if let Ok(true) = std::fs::exists(p_str) {
625                        return true;
626                    }
627                }
628            }
629            false
630        }
631
632        text.remove_tags(*TAGGER, ..);
633
634        let command = text.to_string();
635        let Some(caller) = command.split_whitespace().next() else {
636            return text;
637        };
638
639        let args = cmd::ArgsIter::new(&command);
640
641        let (caller_id, args_id) = if is_in_path(caller) {
642            (form::id_of!("caller.info"), form::id_of!("param.info"))
643        } else {
644            (form::id_of!("caller.error"), form::id_of!("param.error"))
645        };
646
647        let c_s = command.len() - command.trim_start().len();
648        text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
649
650        for (_, range, _) in args {
651            text.insert_tag(*TAGGER, range, args_id.to_tag(0));
652        }
653
654        text
655    }
656
657    fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
658        use std::process::{Command, Stdio};
659
660        let command = text.to_string();
661        let Some(caller) = command.split_whitespace().next() else {
662            return;
663        };
664
665        let handle = context::current_buffer(pa);
666        handle.edit_all(pa, |mut c| {
667            let Ok(mut child) = Command::new(caller)
668                .args(cmd::ArgsIter::new(&command).map(|(a, ..)| a))
669                .stdin(Stdio::piped())
670                .stdout(Stdio::piped())
671                .spawn()
672            else {
673                return;
674            };
675
676            let input = c.selection().to_string();
677            if let Some(mut stdin) = child.stdin.take() {
678                std::thread::spawn(move || {
679                    stdin.write_all(input.as_bytes()).unwrap();
680                });
681            }
682            if let Ok(out) = child.wait_with_output() {
683                let out = String::from_utf8_lossy(&out.stdout);
684                c.set_anchor_if_needed();
685                c.replace(out);
686            }
687        });
688    }
689
690    fn prompt(&self) -> Text {
691        txt!("[prompt]pipe")
692    }
693}
694
695type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;
696
697#[derive(Clone, Eq)]
698enum Completion {
699    Caller,
700    Parameters(Vec<TypeId>),
701}
702
703impl PartialEq for Completion {
704    fn eq(&self, other: &Self) -> bool {
705        match (self, other) {
706            (Self::Parameters(l0), Self::Parameters(r0)) => {
707                l0.iter().all(|param| r0.contains(param))
708                    && r0.iter().all(|param| l0.contains(param))
709            }
710            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
711        }
712    }
713}