use super::utils::{coerce_crlf, estimate_required_lines, line_width};
use crate::{
menu::{Menu, ReedlineMenu},
prompt::PromptEditMode,
Prompt, PromptHistorySearch,
};
use std::borrow::Cow;
#[derive(Debug)]
pub(crate) struct PromptLines<'prompt> {
pub(crate) prompt_str_left: Cow<'prompt, str>,
pub(crate) prompt_str_right: Cow<'prompt, str>,
pub(crate) prompt_indicator: Cow<'prompt, str>,
pub(crate) before_cursor: Cow<'prompt, str>,
pub(crate) after_cursor: Cow<'prompt, str>,
pub(crate) hint: Cow<'prompt, str>,
pub(crate) right_prompt_on_last_line: bool,
}
impl<'prompt> PromptLines<'prompt> {
pub fn new(
prompt: &'prompt dyn Prompt,
prompt_mode: PromptEditMode,
history_indicator: Option<PromptHistorySearch>,
before_cursor: &'prompt str,
after_cursor: &'prompt str,
hint: &'prompt str,
) -> Self {
let prompt_str_left = prompt.render_prompt_left();
let prompt_str_right = prompt.render_prompt_right();
let prompt_indicator = match history_indicator {
Some(prompt_search) => prompt.render_prompt_history_search_indicator(prompt_search),
None => prompt.render_prompt_indicator(prompt_mode),
};
let before_cursor = coerce_crlf(before_cursor);
let after_cursor = coerce_crlf(after_cursor);
let hint = coerce_crlf(hint);
let right_prompt_on_last_line = prompt.right_prompt_on_last_line();
Self {
prompt_str_left,
prompt_str_right,
prompt_indicator,
before_cursor,
after_cursor,
hint,
right_prompt_on_last_line,
}
}
pub(crate) fn required_lines(
&self,
terminal_columns: u16,
before_cursor: bool,
menu: Option<&ReedlineMenu>,
) -> u16 {
let mut input =
self.prompt_str_left.to_string() + &self.prompt_indicator + &self.before_cursor;
if !before_cursor {
input += &self.after_cursor;
if menu.is_none() {
input += &self.hint;
}
}
let lines = estimate_required_lines(&input, terminal_columns);
if let Some(menu) = menu {
lines as u16 + menu.menu_required_lines(terminal_columns)
} else {
lines as u16
}
}
pub(crate) fn distance_from_prompt(&self, terminal_columns: u16) -> u16 {
let input = self.prompt_str_left.to_string() + &self.prompt_indicator + &self.before_cursor;
let lines = estimate_required_lines(&input, terminal_columns);
lines.saturating_sub(1) as u16
}
pub(crate) fn cursor_pos(&self, terminal_columns: u16) -> (u16, u16) {
let prompt_str = format!("{}{}", self.prompt_str_left, self.prompt_indicator);
let last_prompt_str = prompt_str.lines().last().unwrap_or_default();
let is_multiline = self.before_cursor.contains('\n');
let buffer_width = line_width(self.before_cursor.lines().last().unwrap_or_default());
let total_width = if is_multiline {
buffer_width
} else {
buffer_width + line_width(last_prompt_str)
};
let buffer_width_prompt = format!("{}{}", last_prompt_str, self.before_cursor);
let cursor_y = (estimate_required_lines(&buffer_width_prompt, terminal_columns) as u16)
.saturating_sub(1);
let cursor_x = (total_width % terminal_columns as usize) as u16;
(cursor_x, cursor_y as u16)
}
pub(crate) fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 {
let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator;
let lines = estimate_required_lines(&complete_prompt, screen_width);
lines.saturating_sub(1) as u16
}
pub(crate) fn estimate_right_prompt_line_width(&self, terminal_columns: u16) -> u16 {
let first_line_left_prompt = self.prompt_str_left.lines().next();
let last_line_left_prompt = self.prompt_str_left.lines().last();
let prompt_lines_total = self.before_cursor.to_string() + &self.after_cursor + &self.hint;
let prompt_lines_first = prompt_lines_total.lines().next();
let mut estimate = 0;
if self.right_prompt_on_last_line {
if let Some(last_line_left_prompt) = last_line_left_prompt {
estimate += line_width(last_line_left_prompt);
estimate += line_width(&self.prompt_indicator);
if let Some(prompt_lines_first) = prompt_lines_first {
estimate += line_width(prompt_lines_first);
}
}
} else {
let required_lines = estimate_required_lines(&self.prompt_str_left, terminal_columns);
if let Some(first_line_left_prompt) = first_line_left_prompt {
estimate += line_width(first_line_left_prompt);
}
if required_lines == 1 {
estimate += line_width(&self.prompt_indicator);
if let Some(prompt_lines_first) = prompt_lines_first {
estimate += line_width(prompt_lines_first);
}
}
}
if estimate > u16::MAX as usize {
u16::MAX
} else {
estimate as u16
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(
"~/path/",
"❯ ",
"",
100,
(9, 0)
)]
#[case(
"~/longer/path/\n",
"❯ ",
"test",
100,
(6, 0)
)]
#[case(
"~/longer/path/",
"\n❯ ",
"test",
100,
(6, 0)
)]
#[case(
"~/longer/path/\n",
"\n❯ ",
"test",
100,
(6, 0)
)]
#[case(
"~/path/",
"❯ ",
"very long input that does not fit in a single line",
40,
(19, 1)
)]
#[case(
"~/path/\n",
"\n❯\n ",
"very long input that does not fit in a single line",
10,
(1, 5)
)]
#[case(
"~/path/",
"❯ ",
"this is a text that contains newlines\n::: and a multiline prompt",
40,
(26, 2)
)]
#[case(
"~/path/",
"❯ ",
"this is a text that contains newlines\n::: and very loooooooooooooooong text that wraps",
40,
(8, 3)
)]
fn test_cursor_pos(
#[case] prompt_str_left: &str,
#[case] prompt_indicator: &str,
#[case] before_cursor: &str,
#[case] terminal_columns: u16,
#[case] expected: (u16, u16),
) {
let prompt_lines = PromptLines {
prompt_str_left: Cow::Borrowed(prompt_str_left),
prompt_str_right: Cow::Borrowed(""),
prompt_indicator: Cow::Borrowed(prompt_indicator),
before_cursor: Cow::Borrowed(before_cursor),
after_cursor: Cow::Borrowed(""),
hint: Cow::Borrowed(""),
right_prompt_on_last_line: false,
};
let pos = prompt_lines.cursor_pos(terminal_columns);
assert_eq!(pos, expected);
}
}