Skip to main content

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