ad_editor/editor/
minibuffer.rs

1//! A transient buffer for handling interactive input from the user without
2//! modifying the current buffer state.
3//!
4//! Conceptually this is operates as an embedded dmenu.
5use crate::{
6    Config,
7    buffer::{Buffer, Buffers, GapBuffer, Slice},
8    config_handle,
9    dot::TextObject,
10    editor::{Action, Actions, Editor},
11    key::{Arrow, Input},
12    system::System,
13};
14use ad_event::Source;
15use std::{
16    cmp::{self, min},
17    fmt,
18    path::Path,
19    sync::{Arc, RwLock},
20};
21use tracing::trace;
22
23const MINIBUFFER_ID: usize = usize::MAX - 1;
24
25#[derive(Debug, Default)]
26pub struct MiniBufferState<'a> {
27    pub(crate) cx: usize,
28    pub(crate) n_visible_lines: usize,
29    pub(crate) selected_line_idx: usize,
30    pub(crate) prompt: &'a str,
31    pub(crate) input: Slice<'a>,
32    pub(crate) b: Option<&'a Buffer>,
33    pub(crate) top: usize,
34    pub(crate) bottom: usize,
35}
36
37pub(crate) enum MiniBufferSelection {
38    Line { cy: usize, line: String },
39    UserInput { input: String },
40    Cancelled,
41}
42
43/// A mini-buffer always has a single line prompt for accepting user input
44/// with the rest of the buffer content not being directly editable.
45///
46/// Conceptually this is operates as an embedded dmenu.
47pub(crate) struct MiniBuffer<F>
48where
49    F: Fn(&GapBuffer) -> Option<Vec<String>>,
50{
51    on_change: F,
52    prompt: String,
53    n_prompt_chars: usize,
54    input: Buffer,
55    initial_lines: Vec<String>,
56    line_indices: Vec<usize>,
57    b: Buffer,
58    max_height: usize,
59    y: usize,
60    selected_line_idx: usize,
61    n_visible_lines: usize,
62    top: usize,
63    bottom: usize,
64    show_buffer_content: bool,
65}
66
67impl<F> fmt::Debug for MiniBuffer<F>
68where
69    F: Fn(&GapBuffer) -> Option<Vec<String>>,
70{
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        f.debug_struct("MiniBuffer")
73            .field("prompt", &self.prompt)
74            .field("input", &self.input)
75            .finish()
76    }
77}
78
79impl<F> MiniBuffer<F>
80where
81    F: Fn(&GapBuffer) -> Option<Vec<String>>,
82{
83    pub fn new(
84        prompt: String,
85        lines: Vec<String>,
86        max_height: usize,
87        on_change: F,
88        config: Arc<RwLock<Config>>,
89    ) -> Self {
90        let line_indices = Vec::with_capacity(lines.len());
91        let n_prompt_chars = prompt.chars().count();
92
93        Self {
94            on_change,
95            prompt,
96            n_prompt_chars,
97            input: Buffer::new_unnamed(MINIBUFFER_ID, "", config.clone()),
98            initial_lines: lines,
99            line_indices,
100            b: Buffer::new_minibuffer(config),
101            max_height,
102            y: 0,
103            selected_line_idx: 0,
104            n_visible_lines: 0,
105            top: 0,
106            bottom: 0,
107            show_buffer_content: true,
108        }
109    }
110
111    #[inline]
112    fn handle_on_change(&mut self) {
113        if let Some(lines) = (self.on_change)(&self.input.txt) {
114            self.b.txt = GapBuffer::from(lines.join("\n"));
115            self.b.dot.clamp_idx(self.b.txt.len_chars());
116        };
117    }
118
119    #[inline]
120    fn update_state(&mut self) {
121        self.b.txt.clear();
122        self.line_indices.clear();
123
124        let input_fragments: Vec<&str> = self.input.txt.as_str().split_whitespace().collect();
125        let mut visible_lines = vec![];
126
127        for (i, line) in self.initial_lines.iter().enumerate() {
128            let matching = input_fragments.iter().all(|f| {
129                if f.chars().all(|c| c.is_lowercase()) {
130                    line.to_lowercase().contains(f)
131                } else {
132                    line.contains(f)
133                }
134            });
135
136            if matching {
137                visible_lines.push(line.clone());
138                self.line_indices.push(i);
139            }
140        }
141
142        self.b.txt = GapBuffer::from(visible_lines.join("\n"));
143        self.b.dot.clamp_idx(self.b.txt.len_chars());
144
145        let n_visible_lines = min(visible_lines.len(), self.max_height);
146        let (y, _) = self.b.dot.active_cur().as_yx(&self.b);
147
148        let (selected_line_idx, top, bottom, show_buffer_content) = if n_visible_lines == 0 {
149            (0, 0, 0, false)
150        } else if y >= n_visible_lines {
151            let lower = y.saturating_sub(n_visible_lines) + 1;
152            (y, lower, y, true)
153        } else {
154            (y, 0, n_visible_lines - 1, true)
155        };
156
157        self.show_buffer_content = show_buffer_content;
158        self.selected_line_idx = selected_line_idx;
159        self.n_visible_lines = n_visible_lines;
160        self.y = y;
161        self.top = top;
162        self.bottom = bottom;
163    }
164
165    #[inline]
166    fn current_state(&self) -> MiniBufferState<'_> {
167        MiniBufferState {
168            cx: self.input.dot.active_cur().idx + self.n_prompt_chars,
169            n_visible_lines: self.n_visible_lines,
170            prompt: &self.prompt,
171            input: self.input.txt.as_slice(),
172            selected_line_idx: self.selected_line_idx,
173            b: if self.show_buffer_content {
174                Some(&self.b)
175            } else {
176                None
177            },
178            top: self.top,
179            bottom: self.bottom,
180        }
181    }
182
183    #[inline]
184    fn handle_input(&mut self, input: Input) -> Option<MiniBufferSelection> {
185        match input {
186            Input::Char(c) => {
187                self.input
188                    .handle_action(Action::InsertChar { c }, Source::Keyboard);
189                self.handle_on_change();
190            }
191            Input::Ctrl('h') | Input::Backspace | Input::Del => {
192                self.input.handle_action(
193                    Action::DotSet(TextObject::Arr(Arrow::Left), 1),
194                    Source::Keyboard,
195                );
196                self.input.handle_action(Action::Delete, Source::Keyboard);
197                self.handle_on_change();
198            }
199
200            Input::Esc => return Some(MiniBufferSelection::Cancelled),
201            Input::Return => {
202                let selection = match self.b.line(self.y) {
203                    Some(_) if self.line_indices.is_empty() => MiniBufferSelection::UserInput {
204                        input: self.input.txt.to_string(),
205                    },
206                    Some(l) => MiniBufferSelection::Line {
207                        cy: self.line_indices[self.y],
208                        line: l.to_string(),
209                    },
210                    None => MiniBufferSelection::UserInput {
211                        input: self.input.txt.to_string(),
212                    },
213                };
214                return Some(selection);
215            }
216
217            Input::Alt('h') | Input::Arrow(Arrow::Left) => {
218                self.input.handle_action(
219                    Action::DotSet(TextObject::Arr(Arrow::Left), 1),
220                    Source::Keyboard,
221                );
222            }
223            Input::Alt('l') | Input::Arrow(Arrow::Right) => {
224                self.input.handle_action(
225                    Action::DotSet(TextObject::Arr(Arrow::Right), 1),
226                    Source::Keyboard,
227                );
228            }
229            Input::Alt('k') | Input::Arrow(Arrow::Up) => {
230                if self.selected_line_idx == 0 {
231                    self.b.set_dot(TextObject::BufferEnd, 1);
232                } else {
233                    self.b.set_dot(TextObject::Arr(Arrow::Up), 1);
234                }
235            }
236            Input::Alt('j') | Input::Arrow(Arrow::Down) => {
237                if self.selected_line_idx == self.b.len_lines() - 1 {
238                    self.b.set_dot(TextObject::BufferStart, 1);
239                } else {
240                    self.b.set_dot(TextObject::Arr(Arrow::Down), 1);
241                }
242            }
243
244            _ => (),
245        }
246
247        None
248    }
249}
250
251impl<S> Editor<S>
252where
253    S: System,
254{
255    fn prompt_w_callback<F: Fn(&GapBuffer) -> Option<Vec<String>>>(
256        &mut self,
257        prompt: &str,
258        initial_lines: Vec<String>,
259        initial_input: Option<String>,
260        on_change: F,
261    ) -> MiniBufferSelection {
262        let mut mb = MiniBuffer::new(
263            prompt.to_string(),
264            initial_lines,
265            config_handle!(self).minibuffer_lines,
266            on_change,
267            self.config.clone(),
268        );
269
270        if let Some(s) = initial_input {
271            mb.input
272                .handle_action(Action::InsertString { s }, Source::Fsys);
273        }
274
275        while self.running {
276            mb.update_state();
277            self.refresh_screen_w_minibuffer(Some(mb.current_state()));
278            let inputs = self.block_for_input();
279            for input in inputs.into_iter() {
280                if let Some(selection) = mb.handle_input(input) {
281                    return selection;
282                }
283            }
284        }
285
286        MiniBufferSelection::Cancelled
287    }
288
289    /// Use the minibuffer to prompt for user input
290    pub(crate) fn minibuffer_prompt(&mut self, prompt: &str) -> Option<String> {
291        trace!(%prompt, "opening mini-buffer");
292        match self.prompt_w_callback(prompt, vec![], None, |_| None) {
293            MiniBufferSelection::UserInput { input } => Some(input),
294            _ => None,
295        }
296    }
297
298    /// Append ", continue? [y/n]: " to the prompt and return true if the user enters one of
299    /// y, Y, yes, YES, Yes (otherwise return false)
300    pub(crate) fn minibuffer_confirm(&mut self, prompt: &str) -> bool {
301        let resp = self.minibuffer_prompt(&format!("{prompt}, continue? [y/n]: "));
302
303        matches!(resp.as_deref(), Some("y" | "Y" | "yes"))
304    }
305
306    /// Use a [MiniBuffer] to select from a list of strings.
307    pub(crate) fn minibuffer_select_from(
308        &mut self,
309        prompt: &str,
310        initial_lines: Vec<String>,
311    ) -> MiniBufferSelection {
312        self.prompt_w_callback(prompt, initial_lines, None, |_| None)
313    }
314
315    /// Use a [MiniBuffer] to select from the newline delimited output of running a shell command.
316    pub(crate) fn minibuffer_select_from_command_output(
317        &mut self,
318        prompt: &str,
319        cmd: &str,
320        dir: &Path,
321    ) -> MiniBufferSelection {
322        let initial_lines =
323            match self
324                .system
325                .run_command_blocking(cmd, dir, self.active_buffer_id())
326            {
327                Ok(s) => s.lines().map(String::from).collect(),
328                Err(e) => {
329                    self.set_status_message(format!("unable to get minibuffer input: {e}"));
330                    return MiniBufferSelection::Cancelled;
331                }
332            };
333
334        self.prompt_w_callback(prompt, initial_lines, None, |_| None)
335    }
336}
337
338/// Something that can be used to open a minibuffer and run subsequent actions based on
339/// a selection.
340pub(crate) trait MbSelect: Send + Sync {
341    fn clone_selector(&self) -> MbSelector;
342    fn prompt_and_options(&self, buffers: &Buffers) -> (String, Vec<String>);
343    fn selected_actions(&self, sel: MiniBufferSelection) -> Option<Actions>;
344
345    #[allow(unused_variables)]
346    fn initial_input(&self, buffers: &Buffers) -> Option<String> {
347        None
348    }
349
350    fn into_selector(self) -> MbSelector
351    where
352        Self: Sized + 'static,
353    {
354        MbSelector(Box::new(self))
355    }
356}
357
358pub struct MbSelector(Box<dyn MbSelect>);
359
360impl fmt::Debug for MbSelector {
361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362        f.debug_struct("MbSelector").finish()
363    }
364}
365
366impl Clone for MbSelector {
367    fn clone(&self) -> Self {
368        self.0.clone_selector()
369    }
370}
371
372impl cmp::Eq for MbSelector {}
373impl cmp::PartialEq for MbSelector {
374    fn eq(&self, _: &Self) -> bool {
375        true
376    }
377}
378
379impl MbSelector {
380    pub(crate) fn run<S>(&self, ed: &mut Editor<S>)
381    where
382        S: System,
383    {
384        let (prompt, options) = self.0.prompt_and_options(ed.layout.buffers());
385        let initial_input = self.0.initial_input(ed.layout.buffers());
386        let selection = ed.prompt_w_callback(&prompt, options, initial_input, |_| None);
387        if let Some(actions) = self.0.selected_actions(selection) {
388            ed.handle_actions(actions, Source::Fsys);
389        }
390    }
391}