mirror of
https://github.com/nushell/nushell.git
synced 2025-06-11 04:26:49 +02:00
feat: Use reedline for input implementation (#15369)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR replaces the default `input` implementation with `reedline`. It provides a fully backwards compatible implementation, by leveraging left prompt provided through `input "my-prompt> "` provided by reedline. The default indicator is hidden to be fully backwards compatible, the multiline indicator is kept. The legacy implementation will be used when the user passes options truncating input such as `--bytes-until` or `--numchar` or `--suppress-output` which I didn't find a straightforward implementation through reedline. # User-Facing Changes No breaking change. - Adds ability to enter multi-line input with reedline. - Adds ability to pass a command history through the pipe `["command", "history"] | input`- Adds ability to pass a history file through the params `input --history-file path/to/history` # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --> --------- Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
parent
90afb65329
commit
a8c49857d9
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3715,6 +3715,7 @@ dependencies = [
|
||||
"rand 0.9.0",
|
||||
"rand_chacha 0.9.0",
|
||||
"rayon",
|
||||
"reedline",
|
||||
"rmp",
|
||||
"roxmltree",
|
||||
"rstest",
|
||||
|
@ -85,6 +85,7 @@ quick-xml = { workspace = true }
|
||||
rand = { workspace = true, optional = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
rayon = { workspace = true }
|
||||
reedline = { workspace = true, optional = true }
|
||||
roxmltree = { workspace = true }
|
||||
rusqlite = { workspace = true, features = [
|
||||
"bundled",
|
||||
@ -180,6 +181,7 @@ os = [
|
||||
"notify-debouncer-full",
|
||||
"open",
|
||||
"os_pipe",
|
||||
"reedline",
|
||||
"uu_cp",
|
||||
"uu_mkdir",
|
||||
"uu_mktemp",
|
||||
|
@ -1,19 +1,14 @@
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{self, ClearType},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use crate::platform::input::legacy_input::LegacyInput;
|
||||
use crate::platform::input::reedline_prompt::ReedlinePrompt;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::{self, io::IoError};
|
||||
|
||||
use std::{io::Write, time::Duration};
|
||||
use reedline::{FileBackedHistory, HISTORY_SIZE, History, HistoryItem, Reedline, Signal};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Input;
|
||||
|
||||
impl LegacyInput for Input {}
|
||||
|
||||
impl Command for Input {
|
||||
fn name(&self) -> &str {
|
||||
"input"
|
||||
@ -29,7 +24,9 @@ impl Command for Input {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("input")
|
||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||
.input_output_types(vec![
|
||||
(Type::Nothing, Type::Any),
|
||||
(Type::List(Box::new(Type::String)), Type::Any)])
|
||||
.allow_variants_without_examples(true)
|
||||
.optional("prompt", SyntaxShape::String, "Prompt to show the user.")
|
||||
.named(
|
||||
@ -50,6 +47,23 @@ impl Command for Input {
|
||||
"default value if no input is provided",
|
||||
Some('d'),
|
||||
)
|
||||
.switch(
|
||||
"reedline",
|
||||
"use the reedline library, defaults to false",
|
||||
None
|
||||
)
|
||||
.named(
|
||||
"history-file",
|
||||
SyntaxShape::Filepath,
|
||||
"Path to a file to read and write command history. This is a text file and will be created if it doesn't exist. Will be used as the selection list.",
|
||||
None,
|
||||
)
|
||||
.named(
|
||||
"max-history",
|
||||
SyntaxShape::Int,
|
||||
"The maximum number of entries to keep in the history, defaults to $env.config.history.max_size.",
|
||||
None,
|
||||
)
|
||||
.switch("suppress-output", "don't print keystroke values", Some('s'))
|
||||
.category(Category::Platform)
|
||||
}
|
||||
@ -59,116 +73,127 @@ impl Command for Input {
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
|
||||
let bytes_until: Option<String> = call.get_flag(engine_state, stack, "bytes-until-any")?;
|
||||
let suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
|
||||
let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
||||
let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
|
||||
item: i64::MAX,
|
||||
span: call.head,
|
||||
});
|
||||
// Check if we should use the legacy implementation or the reedline implementation
|
||||
let use_reedline = [
|
||||
// reedline is not set - use legacy implementation
|
||||
call.has_flag(engine_state, stack, "reedline")?,
|
||||
// We have the history-file or max-history flags set to None
|
||||
call.get_flag::<String>(engine_state, stack, "history-file")?
|
||||
.is_some(),
|
||||
call.get_flag::<i64>(engine_state, stack, "max-history")?
|
||||
.is_some(),
|
||||
]
|
||||
.iter()
|
||||
.any(|x| *x);
|
||||
|
||||
let from_io_error = IoError::factory(call.head, None);
|
||||
|
||||
if numchar.item < 1 {
|
||||
return Err(ShellError::UnsupportedInput {
|
||||
msg: "Number of characters to read has to be positive".to_string(),
|
||||
input: "value originated from here".to_string(),
|
||||
msg_span: call.head,
|
||||
input_span: numchar.span,
|
||||
});
|
||||
if !use_reedline {
|
||||
return self.legacy_input(engine_state, stack, call, input);
|
||||
}
|
||||
|
||||
let prompt_str: Option<String> = call.opt(engine_state, stack, 0)?;
|
||||
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
|
||||
if let Some(prompt) = &prompt {
|
||||
match &default_val {
|
||||
None => print!("{prompt}"),
|
||||
Some(val) => print!("{prompt} (default: {val})"),
|
||||
let history_file_val: Option<String> =
|
||||
call.get_flag(engine_state, stack, "history-file")?;
|
||||
let max_history: usize = call
|
||||
.get_flag::<i64>(engine_state, stack, "max-history")?
|
||||
.map(|l| if l < 0 { 0 } else { l as usize })
|
||||
.unwrap_or(HISTORY_SIZE);
|
||||
let max_history_span = call.get_flag_span(stack, "max-history");
|
||||
let history_file_span = call.get_flag_span(stack, "history-file");
|
||||
|
||||
let default_str = match (&prompt_str, &default_val) {
|
||||
(Some(_prompt), Some(val)) => format!("(default: {val}) "),
|
||||
_ => "".to_string(),
|
||||
};
|
||||
|
||||
let history_entries = match input {
|
||||
PipelineData::Value(Value::List { vals, .. }, ..) => Some(vals),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// If we either have history entries or history file, we create an history
|
||||
let history = match (history_entries.is_some(), history_file_val.is_some()) {
|
||||
(false, false) => None, // Neither are set, no need for history support
|
||||
_ => {
|
||||
let file_history = match history_file_val {
|
||||
Some(file) => FileBackedHistory::with_file(max_history, file.into()),
|
||||
None => FileBackedHistory::new(max_history),
|
||||
};
|
||||
let mut history = match file_history {
|
||||
Ok(h) => h,
|
||||
Err(e) => match e.0 {
|
||||
reedline::ReedlineErrorVariants::IOError(err) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: err.to_string(),
|
||||
val_span: history_file_span.expect("history-file should be set"),
|
||||
call_span: call.head,
|
||||
});
|
||||
}
|
||||
reedline::ReedlineErrorVariants::OtherHistoryError(msg) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: msg.to_string(),
|
||||
val_span: max_history_span.expect("max-history should be set"),
|
||||
call_span: call.head,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "unable to create history".to_string(),
|
||||
val_span: call.head,
|
||||
call_span: call.head,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(vals) = history_entries {
|
||||
vals.iter().for_each(|val| {
|
||||
if let Value::String { val, .. } = val {
|
||||
let _ = history.save(HistoryItem::from_command_line(val.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(history)
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
};
|
||||
|
||||
let prompt = ReedlinePrompt {
|
||||
indicator: default_str,
|
||||
left_prompt: prompt_str.unwrap_or("".to_string()),
|
||||
right_prompt: "".to_string(),
|
||||
};
|
||||
|
||||
let mut line_editor = Reedline::create();
|
||||
line_editor = line_editor.with_ansi_colors(false);
|
||||
line_editor = match history {
|
||||
Some(h) => line_editor.with_history(Box::new(h)),
|
||||
None => line_editor,
|
||||
};
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?;
|
||||
// clear terminal events
|
||||
while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
|
||||
// If there's an event, read it to remove it from the queue
|
||||
let _ = crossterm::event::read().map_err(&from_io_error)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
|
||||
break;
|
||||
match line_editor.read_line(&prompt) {
|
||||
Ok(Signal::Success(buffer)) => {
|
||||
buf.push_str(&buffer);
|
||||
}
|
||||
match crossterm::event::read() {
|
||||
Ok(Event::Key(k)) => match k.kind {
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => {
|
||||
match k.code {
|
||||
// TODO: maintain keycode parity with existing command
|
||||
KeyCode::Char(c) => {
|
||||
if k.modifiers == KeyModifiers::ALT
|
||||
|| k.modifiers == KeyModifiers::CONTROL
|
||||
{
|
||||
if k.modifiers == KeyModifiers::CONTROL && c == 'c' {
|
||||
crossterm::terminal::disable_raw_mode()
|
||||
.map_err(&from_io_error)?;
|
||||
return Err(IoError::new(
|
||||
shell_error::io::ErrorKind::from_std(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
),
|
||||
call.head,
|
||||
None,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bytes_until) = bytes_until.as_ref() {
|
||||
if bytes_until.bytes().contains(&(c as u8)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let _ = buf.pop();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
Ok(_) => continue,
|
||||
Err(event_error) => {
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
return Err(from_io_error(event_error).into());
|
||||
}
|
||||
}
|
||||
if !suppress_output {
|
||||
// clear the current line and print the current buffer
|
||||
execute!(
|
||||
std::io::stdout(),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
cursor::MoveToColumn(0),
|
||||
Ok(Signal::CtrlC) => {
|
||||
return Err(IoError::new(
|
||||
shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Interrupted),
|
||||
call.head,
|
||||
None,
|
||||
)
|
||||
.map_err(|err| IoError::new(err, call.head, None))?;
|
||||
if let Some(prompt) = &prompt {
|
||||
execute!(std::io::stdout(), Print(prompt.to_string()))
|
||||
.map_err(&from_io_error)?;
|
||||
}
|
||||
execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?;
|
||||
.into());
|
||||
}
|
||||
Ok(Signal::CtrlD) => {
|
||||
// Do nothing on ctrl-d
|
||||
return Ok(Value::nothing(call.head).into_pipeline_data());
|
||||
}
|
||||
Err(event_error) => {
|
||||
let from_io_error = IoError::factory(call.head, None);
|
||||
return Err(from_io_error(event_error).into());
|
||||
}
|
||||
}
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
if !suppress_output {
|
||||
std::io::stdout().write_all(b"\n").map_err(&from_io_error)?;
|
||||
}
|
||||
match default_val {
|
||||
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
||||
@ -193,6 +218,16 @@ impl Command for Input {
|
||||
example: "let user_input = (input --default 10)",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Get input from the user with history, and assign to a variable",
|
||||
example: "let user_input = ([past,command,entries] | input )",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Get input from the user with history backed by a file, and assign to a variable",
|
||||
example: "let user_input = (input --history-file ./history.txt)",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
136
crates/nu-command/src/platform/input/legacy_input.rs
Normal file
136
crates/nu-command/src/platform/input/legacy_input.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{Event, KeyCode, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
style::Print,
|
||||
terminal::{self, ClearType},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::shell_error::{self, io::IoError};
|
||||
|
||||
use std::{io::Write, time::Duration};
|
||||
|
||||
pub trait LegacyInput {
|
||||
fn legacy_input(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
|
||||
let bytes_until: Option<String> = call.get_flag(engine_state, stack, "bytes-until-any")?;
|
||||
let suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
|
||||
let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
|
||||
let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
|
||||
item: i64::MAX,
|
||||
span: call.head,
|
||||
});
|
||||
|
||||
let from_io_error = IoError::factory(call.head, None);
|
||||
|
||||
if numchar.item < 1 {
|
||||
return Err(ShellError::UnsupportedInput {
|
||||
msg: "Number of characters to read has to be positive".to_string(),
|
||||
input: "value originated from here".to_string(),
|
||||
msg_span: call.head,
|
||||
input_span: numchar.span,
|
||||
});
|
||||
}
|
||||
|
||||
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
|
||||
if let Some(prompt) = &prompt {
|
||||
match &default_val {
|
||||
None => print!("{prompt}"),
|
||||
Some(val) => print!("{prompt} (default: {val})"),
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?;
|
||||
// clear terminal events
|
||||
while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
|
||||
// If there's an event, read it to remove it from the queue
|
||||
let _ = crossterm::event::read().map_err(&from_io_error)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
|
||||
break;
|
||||
}
|
||||
match crossterm::event::read() {
|
||||
Ok(Event::Key(k)) => match k.kind {
|
||||
KeyEventKind::Press | KeyEventKind::Repeat => {
|
||||
match k.code {
|
||||
// TODO: maintain keycode parity with existing command
|
||||
KeyCode::Char(c) => {
|
||||
if k.modifiers == KeyModifiers::ALT
|
||||
|| k.modifiers == KeyModifiers::CONTROL
|
||||
{
|
||||
if k.modifiers == KeyModifiers::CONTROL && c == 'c' {
|
||||
crossterm::terminal::disable_raw_mode()
|
||||
.map_err(&from_io_error)?;
|
||||
return Err(IoError::new(
|
||||
shell_error::io::ErrorKind::from_std(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
),
|
||||
call.head,
|
||||
None,
|
||||
)
|
||||
.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bytes_until) = bytes_until.as_ref() {
|
||||
if bytes_until.bytes().contains(&(c as u8)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
buf.push(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let _ = buf.pop();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
Ok(_) => continue,
|
||||
Err(event_error) => {
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
return Err(from_io_error(event_error).into());
|
||||
}
|
||||
}
|
||||
if !suppress_output {
|
||||
// clear the current line and print the current buffer
|
||||
execute!(
|
||||
std::io::stdout(),
|
||||
terminal::Clear(ClearType::CurrentLine),
|
||||
cursor::MoveToColumn(0),
|
||||
)
|
||||
.map_err(|err| IoError::new(err, call.head, None))?;
|
||||
if let Some(prompt) = &prompt {
|
||||
execute!(std::io::stdout(), Print(prompt.to_string()))
|
||||
.map_err(&from_io_error)?;
|
||||
}
|
||||
execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?;
|
||||
}
|
||||
}
|
||||
crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?;
|
||||
if !suppress_output {
|
||||
std::io::stdout().write_all(b"\n").map_err(&from_io_error)?;
|
||||
}
|
||||
match default_val {
|
||||
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
||||
_ => Ok(Value::string(buf, call.head).into_pipeline_data()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
mod input_;
|
||||
mod input_listen;
|
||||
mod legacy_input;
|
||||
mod list;
|
||||
mod reedline_prompt;
|
||||
|
||||
pub use input_::Input;
|
||||
pub use input_listen::InputListen;
|
||||
|
62
crates/nu-command/src/platform/input/reedline_prompt.rs
Normal file
62
crates/nu-command/src/platform/input/reedline_prompt.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use reedline::{
|
||||
Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, PromptViMode,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// The default prompt indicator
|
||||
pub static DEFAULT_VI_INSERT_PROMPT_INDICATOR: &str = ": ";
|
||||
pub static DEFAULT_VI_NORMAL_PROMPT_INDICATOR: &str = "〉";
|
||||
pub static DEFAULT_MULTILINE_INDICATOR: &str = "::: ";
|
||||
|
||||
/// Simple [`Prompt`] displaying a configurable left and a right prompt.
|
||||
/// For more fine-tuned configuration, implement the [`Prompt`] trait.
|
||||
/// For the default configuration, use [`DefaultPrompt::default()`]
|
||||
#[derive(Clone)]
|
||||
pub struct ReedlinePrompt {
|
||||
/// What segment should be rendered in the left (main) prompt
|
||||
pub left_prompt: String,
|
||||
pub right_prompt: String,
|
||||
pub indicator: String,
|
||||
}
|
||||
|
||||
impl Prompt for ReedlinePrompt {
|
||||
fn render_prompt_left(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.left_prompt)
|
||||
}
|
||||
|
||||
fn render_prompt_right(&self) -> Cow<str> {
|
||||
Cow::Borrowed(&self.right_prompt)
|
||||
}
|
||||
|
||||
fn render_prompt_indicator(&self, edit_mode: PromptEditMode) -> Cow<str> {
|
||||
match edit_mode {
|
||||
PromptEditMode::Default | PromptEditMode::Emacs => self.indicator.as_str().into(),
|
||||
PromptEditMode::Vi(vi_mode) => match vi_mode {
|
||||
PromptViMode::Normal => DEFAULT_VI_NORMAL_PROMPT_INDICATOR.into(),
|
||||
PromptViMode::Insert => DEFAULT_VI_INSERT_PROMPT_INDICATOR.into(),
|
||||
},
|
||||
PromptEditMode::Custom(str) => format!("({str})").into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_multiline_indicator(&self) -> Cow<str> {
|
||||
Cow::Borrowed(DEFAULT_MULTILINE_INDICATOR)
|
||||
}
|
||||
|
||||
fn render_prompt_history_search_indicator(
|
||||
&self,
|
||||
history_search: PromptHistorySearch,
|
||||
) -> Cow<str> {
|
||||
let prefix = match history_search.status {
|
||||
PromptHistorySearchStatus::Passing => "",
|
||||
PromptHistorySearchStatus::Failing => "failing ",
|
||||
};
|
||||
// NOTE: magic strings, given there is logic on how these compose I am not sure if it
|
||||
// is worth extracting in to static constant
|
||||
Cow::Owned(format!(
|
||||
"({}reverse-search: {}) ",
|
||||
prefix, history_search.term
|
||||
))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user