Skip to main content

reedline/prompt/
base.rs

1use {
2    crate::core_editor::{RestPolicy, SelectionExtent},
3    nu_ansi_term::Color,
4    serde::{Deserialize, Serialize},
5    std::{
6        borrow::Cow,
7        fmt::{Display, Formatter},
8    },
9    strum::{EnumIter, EnumString, IntoDiscriminant},
10};
11
12// The *light* variants are deliberate. Before the nu-ansi-term migration these
13// were crossterm's `Color::Green`/`Color::Cyan`, which are palette 10 and 14;
14// crossterm spells the dark ones `DarkGreen`/`DarkCyan`. nu-ansi-term has no
15// `Dark*` prefix, so its `Green` is palette 2. Naming them here would darken
16// every default prompt.
17
18/// The default color for the prompt
19pub static DEFAULT_PROMPT_COLOR: Color = Color::LightGreen;
20/// The default color for the multiline prompt indicator
21pub static DEFAULT_PROMPT_MULTILINE_COLOR: Color = Color::LightBlue;
22/// The default color for the prompt indicator
23pub static DEFAULT_INDICATOR_COLOR: Color = Color::LightCyan;
24/// The default color for the right prompt
25pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::Purple;
26
27/// The current success/failure of the history search
28pub enum PromptHistorySearchStatus {
29    /// Success for the search
30    Passing,
31
32    /// Failure to find the search
33    Failing,
34}
35
36/// A representation of the history search
37pub struct PromptHistorySearch {
38    /// The status of the search
39    pub status: PromptHistorySearchStatus,
40
41    /// The search term used during the search
42    pub term: String,
43}
44
45impl PromptHistorySearch {
46    /// A constructor to create a history search
47    pub const fn new(status: PromptHistorySearchStatus, search_term: String) -> Self {
48        PromptHistorySearch {
49            status,
50            term: search_term,
51        }
52    }
53}
54
55/// Modes that the prompt can be in
56#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
57pub enum PromptEditMode {
58    /// The default mode
59    #[default]
60    Default,
61
62    /// Emacs normal mode
63    Emacs,
64
65    /// A vi-specific mode
66    Vi(PromptViMode),
67
68    /// A helix/Kakoune like mode
69    #[cfg(feature = "helix")]
70    Helix(PromptHelixMode),
71
72    /// A custom mode
73    Custom(String),
74}
75
76impl PromptEditMode {
77    pub(crate) fn rest_policy(&self) -> RestPolicy {
78        match self {
79            PromptEditMode::Vi(PromptViMode::Normal) => RestPolicy::OnGrapheme,
80            // Visual selections are min-width-1: the cursor always covers at
81            // least the grapheme it sits on, so an empty point widens to a block.
82            PromptEditMode::Vi(PromptViMode::Visual) => RestPolicy::Block,
83            // Helix counts the line terminator as a cell, so `l` reaches it and
84            // `d` there joins the lines; vi visual stops short of it.
85            #[cfg(feature = "helix")]
86            PromptEditMode::Helix(PromptHelixMode::Normal)
87            | PromptEditMode::Helix(PromptHelixMode::Select) => RestPolicy::BlockOverNewline,
88            #[cfg(feature = "helix")]
89            PromptEditMode::Helix(PromptHelixMode::Insert) => RestPolicy::Between,
90            PromptEditMode::Vi(PromptViMode::Insert)
91            | PromptEditMode::Default
92            | PromptEditMode::Emacs => RestPolicy::Between,
93            // No catch-all `_ =>` arm over the variants on purpose: a future
94            // variant then fails to compile here until it is given an explicit
95            // policy, rather than silently defaulting. The `_` below only
96            // ignores the custom mode's name.
97            PromptEditMode::Custom(_) => RestPolicy::Between,
98        }
99    }
100
101    /// Whether an operation *on* the selection leaves it standing.
102    ///
103    /// Helix keeps it, so a yank or a case change can be followed by another
104    /// operation over the same span. Vim drops it: `y` or `~` in visual mode
105    /// returns to normal with nothing selected, and a lingering anchor would
106    /// paint a highlight there.
107    pub(crate) fn retains_selection_after_edit(&self) -> bool {
108        match self {
109            #[cfg(feature = "helix")]
110            PromptEditMode::Helix(_) => true,
111            PromptEditMode::Vi(_)
112            | PromptEditMode::Default
113            | PromptEditMode::Emacs
114            | PromptEditMode::Custom(_) => false,
115        }
116    }
117
118    pub(crate) fn selection_extent(&self) -> SelectionExtent {
119        match self {
120            // Vi normal/visual sweep the block cursor over the grapheme it
121            // lands on (vim's inclusive visual: `vw` selects "foo b").
122            PromptEditMode::Vi(_) => SelectionExtent::CoverLanding,
123            // The bar modes never form a block selection, and `op_end` is
124            // exclusive for the word/line/grapheme motions they emit (a forward
125            // find stays inclusive, matching its operator span), so the
126            // gap-indexed `Span` is the natural reading.
127            #[cfg(feature = "helix")]
128            PromptEditMode::Helix(_) => SelectionExtent::Span,
129            PromptEditMode::Default | PromptEditMode::Emacs | PromptEditMode::Custom(_) => {
130                SelectionExtent::Span
131            }
132        }
133    }
134}
135
136/// The vi-specific modes that the prompt can be in
137#[derive(Serialize, Deserialize, Clone, Debug, EnumIter, Default, PartialEq, Eq)]
138pub enum PromptViMode {
139    /// The default mode
140    #[default]
141    Normal,
142
143    /// Insertion mode
144    Insert,
145
146    /// Visual (selection) mode — like normal, but the cursor carries a
147    /// min-width-1 selection that motions extend.
148    Visual,
149}
150
151/// The helix/Kakoune like modes that the prompt can be in
152#[cfg(feature = "helix")]
153#[derive(Serialize, Deserialize, Clone, Debug, EnumIter, Default, PartialEq, Eq)]
154pub enum PromptHelixMode {
155    /// Normal mode carries an at least 1 grapheme wide selection and
156    /// extends it depending on the motion and its target; both anchor
157    /// and head can move
158    #[default]
159    Normal,
160
161    /// Insert mode is a collapsed cursor between graphemes
162    Insert,
163
164    /// Select mode plants an anchor and extends the selection through
165    /// the head, only the head moves
166    Select,
167}
168
169/// This is the discriminant type for [`PromptEditMode`]
170#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, EnumIter, EnumString)]
171#[strum(ascii_case_insensitive)]
172pub enum PromptEditModeDiscriminants {
173    /// The default mode
174    #[default]
175    Default,
176
177    /// Emacs normal mode
178    Emacs,
179
180    /// Vi normal mode
181    #[strum(serialize = "ViNormal", serialize = "vi_normal")]
182    ViNormal,
183
184    /// Vi insert mode
185    #[strum(serialize = "ViInsert", serialize = "vi_insert")]
186    ViInsert,
187
188    /// Helix normal mode
189    #[cfg(feature = "helix")]
190    #[strum(serialize = "HelixNormal", serialize = "helix_normal")]
191    HelixNormal,
192
193    /// Helix insert mode
194    #[cfg(feature = "helix")]
195    #[strum(serialize = "HelixInsert", serialize = "helix_insert")]
196    HelixInsert,
197
198    /// Helix select mode
199    #[cfg(feature = "helix")]
200    #[strum(serialize = "HelixSelect", serialize = "helix_select")]
201    HelixSelect,
202
203    /// A custom mode
204    Custom,
205}
206
207impl From<PromptViMode> for PromptEditMode {
208    fn from(value: PromptViMode) -> Self {
209        Self::Vi(value)
210    }
211}
212
213impl Display for PromptEditMode {
214    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
215        #[cfg(feature = "helix")]
216        use PromptHelixMode as Helix;
217        use PromptViMode as Vi;
218        match self {
219            Self::Default => write!(f, "Default"),
220            Self::Emacs => write!(f, "Emacs"),
221            Self::Vi(Vi::Normal) => write!(f, "Vi_Normal"),
222            Self::Vi(Vi::Insert) => write!(f, "Vi_Insert"),
223            Self::Vi(Vi::Visual) => write!(f, "Vi_Visual"),
224            #[cfg(feature = "helix")]
225            Self::Helix(Helix::Normal) => write!(f, "Helix_Normal"),
226            #[cfg(feature = "helix")]
227            Self::Helix(Helix::Insert) => write!(f, "Helix_Insert"),
228            #[cfg(feature = "helix")]
229            Self::Helix(Helix::Select) => write!(f, "Helix_Select"),
230            Self::Custom(s) => write!(f, "Custom_{s}"),
231        }
232    }
233}
234
235impl IntoDiscriminant for PromptEditMode {
236    type Discriminant = PromptEditModeDiscriminants;
237
238    fn discriminant(&self) -> Self::Discriminant {
239        #[cfg(feature = "helix")]
240        use PromptHelixMode as Helix;
241        use PromptViMode as Vi;
242        match self {
243            Self::Default => Self::Discriminant::Default,
244            Self::Emacs => Self::Discriminant::Emacs,
245            // Vi visual still shares Normal's discriminant: it uses the
246            // normal-mode keybindings, differing only in selection geometry.
247            // Splitting that pair is its own change.
248            Self::Vi(Vi::Normal | Vi::Visual) => Self::Discriminant::ViNormal,
249            Self::Vi(Vi::Insert) => Self::Discriminant::ViInsert,
250            #[cfg(feature = "helix")]
251            Self::Helix(Helix::Normal) => Self::Discriminant::HelixNormal,
252            #[cfg(feature = "helix")]
253            Self::Helix(Helix::Insert) => Self::Discriminant::HelixInsert,
254            #[cfg(feature = "helix")]
255            Self::Helix(Helix::Select) => Self::Discriminant::HelixSelect,
256            Self::Custom(_) => Self::Discriminant::Custom,
257        }
258    }
259}
260
261/// API to provide a custom prompt.
262///
263/// Implementors have to provide [`str`]-based content which will be
264/// displayed before the `LineBuffer` is drawn.
265pub trait Prompt: Send {
266    /// Provide content of the left full prompt
267    fn render_prompt_left(&self) -> Cow<'_, str>;
268    /// Provide content of the right full prompt
269    fn render_prompt_right(&self) -> Cow<'_, str>;
270    /// Render the prompt indicator (Last part of the prompt that changes based on the editor mode)
271    fn render_prompt_indicator(&self, prompt_mode: PromptEditMode) -> Cow<'_, str>;
272    /// Indicator to show before explicit new lines
273    fn render_prompt_multiline_indicator(&self) -> Cow<'_, str>;
274    /// Render the prompt indicator for `Ctrl-R` history search
275    fn render_prompt_history_search_indicator(
276        &self,
277        history_search: PromptHistorySearch,
278    ) -> Cow<'_, str>;
279    /// Get the default prompt color
280    fn get_prompt_color(&self) -> Color {
281        DEFAULT_PROMPT_COLOR
282    }
283    /// Get the default multiline prompt color
284    fn get_prompt_multiline_color(&self) -> Color {
285        DEFAULT_PROMPT_MULTILINE_COLOR
286    }
287    /// Get the default indicator color
288    fn get_indicator_color(&self) -> Color {
289        DEFAULT_INDICATOR_COLOR
290    }
291    /// Get the default right prompt color
292    fn get_prompt_right_color(&self) -> Color {
293        DEFAULT_PROMPT_RIGHT_COLOR
294    }
295
296    /// Whether to render right prompt on the last line
297    fn right_prompt_on_last_line(&self) -> bool {
298        false
299    }
300}
301
302#[cfg(test)]
303mod test {
304    use super::*;
305
306    #[test]
307    fn selection_extent_maps_vi_to_cover_landing_and_bar_modes_to_span() {
308        // Pin the dispatch table itself: the PR's headline invariant is that vi
309        // stays on `CoverLanding` (a strict noop) while the bar modes move to the
310        // `Span` model. Asserting the mapping here fails loudly at the switch if a
311        // future refactor accidentally reroutes a mode, rather than surfacing as a
312        // downstream selection assertion in some editor test.
313        use PromptViMode::{Insert, Normal, Visual};
314        for mode in [Normal, Insert, Visual] {
315            assert_eq!(
316                PromptEditMode::Vi(mode).selection_extent(),
317                SelectionExtent::CoverLanding,
318            );
319        }
320        assert_eq!(
321            PromptEditMode::Emacs.selection_extent(),
322            SelectionExtent::Span,
323        );
324        assert_eq!(
325            PromptEditMode::Default.selection_extent(),
326            SelectionExtent::Span,
327        );
328        assert_eq!(
329            PromptEditMode::Custom("anything".into()).selection_extent(),
330            SelectionExtent::Span,
331        );
332    }
333
334    #[cfg(feature = "helix")]
335    #[test]
336    fn each_helix_mode_gets_its_own_discriminant() {
337        use PromptEditModeDiscriminants as D;
338        use PromptHelixMode::{Insert, Normal, Select};
339        for (mode, expected) in [
340            (Normal, D::HelixNormal),
341            (Insert, D::HelixInsert),
342            (Select, D::HelixSelect),
343        ] {
344            assert_eq!(PromptEditMode::Helix(mode).discriminant(), expected);
345        }
346    }
347
348    #[test]
349    fn vi_visual_still_shares_the_normal_discriminant() {
350        // Pinned so splitting the vi pair stays a conscious edit.
351        use PromptEditModeDiscriminants as D;
352        assert_eq!(
353            PromptEditMode::Vi(PromptViMode::Visual).discriminant(),
354            D::ViNormal,
355        );
356        assert_eq!(
357            PromptEditMode::Vi(PromptViMode::Normal).discriminant(),
358            D::ViNormal,
359        );
360    }
361
362    #[cfg(feature = "helix")]
363    #[test]
364    fn helix_discriminants_round_trip_through_their_names() {
365        use std::str::FromStr;
366        use PromptEditModeDiscriminants as D;
367        for (name, expected) in [
368            ("helix_normal", D::HelixNormal),
369            ("helix_insert", D::HelixInsert),
370            ("helix_select", D::HelixSelect),
371            ("HelixSelect", D::HelixSelect),
372        ] {
373            assert_eq!(D::from_str(name), Ok(expected), "{name}");
374        }
375    }
376}