use super::{Menu, MenuBuilder, MenuEvent, MenuSettings};
use crate::{
core_editor::Editor,
menu_functions::{
can_partially_complete, completer_input, floor_char_boundary, get_match_indices,
replace_in_buffer, style_suggestion, truncate_with_ansi,
},
painting::Painter,
Completer, Suggestion,
};
use itertools::{
EitherOrBoth::{Both, Left, Right},
Itertools,
};
use nu_ansi_term::ansi::RESET;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub enum DescriptionMode {
Left,
Right,
PreferRight,
}
struct BorderSymbols {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
}
impl Default for BorderSymbols {
fn default() -> Self {
Self {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
}
}
}
struct DefaultIdeMenuDetails {
pub min_completion_width: u16,
pub max_completion_width: u16,
pub max_completion_height: u16,
pub padding: u16,
pub border: Option<BorderSymbols>,
pub cursor_offset: i16,
pub description_mode: DescriptionMode,
pub min_description_width: u16,
pub max_description_width: u16,
pub max_description_height: u16,
pub description_offset: u16,
pub correct_cursor_pos: bool,
}
impl Default for DefaultIdeMenuDetails {
fn default() -> Self {
Self {
min_completion_width: 0,
max_completion_width: 50,
max_completion_height: u16::MAX, padding: 0,
border: None,
cursor_offset: 0,
description_mode: DescriptionMode::PreferRight,
min_description_width: 15,
max_description_width: 50,
max_description_height: 10,
description_offset: 1,
correct_cursor_pos: false,
}
}
}
#[derive(Default)]
struct IdeMenuDetails {
pub cursor_col: u16,
pub menu_width: u16,
pub completion_width: u16,
pub description_width: u16,
pub description_is_right: bool,
pub space_left: u16,
pub space_right: u16,
pub description_offset: u16,
pub shortest_base_string: String,
}
pub struct IdeMenu {
settings: MenuSettings,
active: bool,
default_details: DefaultIdeMenuDetails,
working_details: IdeMenuDetails,
values: Vec<Suggestion>,
display_widths: Vec<usize>,
selected: u16,
skip_values: u16,
event: Option<MenuEvent>,
longest_suggestion: usize,
input: Option<String>,
}
impl Default for IdeMenu {
fn default() -> Self {
Self {
settings: MenuSettings::default().with_name("ide_completion_menu"),
active: false,
default_details: DefaultIdeMenuDetails::default(),
working_details: IdeMenuDetails::default(),
values: Vec::new(),
display_widths: Vec::new(),
selected: 0,
skip_values: 0,
event: None,
longest_suggestion: 0,
input: None,
}
}
}
impl MenuBuilder for IdeMenu {
fn settings_mut(&mut self) -> &mut MenuSettings {
&mut self.settings
}
}
impl IdeMenu {
#[must_use]
pub fn with_min_completion_width(mut self, width: u16) -> Self {
self.default_details.min_completion_width = width;
self
}
#[must_use]
pub fn with_max_completion_width(mut self, width: u16) -> Self {
self.default_details.max_completion_width = width;
self
}
#[must_use]
pub fn with_max_completion_height(mut self, height: u16) -> Self {
self.default_details.max_completion_height = height;
self
}
#[must_use]
pub fn with_padding(mut self, padding: u16) -> Self {
self.default_details.padding = padding;
self
}
#[must_use]
pub fn with_default_border(mut self) -> Self {
self.default_details.border = Some(BorderSymbols::default());
self
}
#[must_use]
pub fn with_border(
mut self,
top_right: char,
top_left: char,
bottom_right: char,
bottom_left: char,
horizontal: char,
vertical: char,
) -> Self {
self.default_details.border = Some(BorderSymbols {
top_right,
top_left,
bottom_right,
bottom_left,
horizontal,
vertical,
});
self
}
#[must_use]
pub fn with_cursor_offset(mut self, cursor_offset: i16) -> Self {
self.default_details.cursor_offset = cursor_offset;
self
}
#[must_use]
pub fn with_description_mode(mut self, description_mode: DescriptionMode) -> Self {
self.default_details.description_mode = description_mode;
self
}
#[must_use]
pub fn with_min_description_width(mut self, min_description_width: u16) -> Self {
self.default_details.min_description_width = min_description_width;
self
}
#[must_use]
pub fn with_max_description_width(mut self, max_description_width: u16) -> Self {
self.default_details.max_description_width = max_description_width;
self
}
#[must_use]
pub fn with_max_description_height(mut self, max_description_height: u16) -> Self {
self.default_details.max_description_height = max_description_height;
self
}
#[must_use]
pub fn with_description_offset(mut self, description_offset: u16) -> Self {
self.default_details.description_offset = description_offset;
self
}
#[must_use]
pub fn with_correct_cursor_pos(mut self, correct_cursor_pos: bool) -> Self {
self.default_details.correct_cursor_pos = correct_cursor_pos;
self
}
}
impl IdeMenu {
fn move_next(&mut self) {
if self.selected < (self.values.len() as u16).saturating_sub(1) {
self.selected += 1;
} else {
self.selected = 0;
}
}
fn move_previous(&mut self) {
if self.selected > 0 {
self.selected -= 1;
} else {
self.selected = self.values.len().saturating_sub(1) as u16;
}
}
fn index(&self) -> usize {
self.selected as usize
}
fn get_value(&self) -> Option<Suggestion> {
self.values.get(self.index()).cloned()
}
fn get_rows(&self) -> u16 {
let mut values = self.get_values().len() as u16;
if values == 0 {
return 1;
}
if self.default_details.border.is_some() {
values += 2;
}
let description_height = self
.get_value()
.and_then(|value| value.description)
.map(|description| {
self.description_dims(
description,
self.working_details.description_width,
self.default_details.max_description_height,
0,
)
.1
})
.unwrap_or(0)
.min(self.default_details.max_description_height);
values.max(description_height)
}
fn reset_position(&mut self) {
self.selected = 0;
}
fn no_records_msg(&self, use_ansi_coloring: bool) -> String {
let msg = "NO RECORDS FOUND";
if use_ansi_coloring {
format!(
"{}{}{}",
self.settings.color.selected_text_style.prefix(),
msg,
RESET
)
} else {
msg.to_string()
}
}
fn create_description(
&self,
description: String,
use_ansi_coloring: bool,
available_width: u16,
available_height: u16,
min_width: u16,
) -> Vec<String> {
if description.is_empty() || available_width == 0 || available_height == 0 {
return Vec::new();
}
let border_width = if self.default_details.border.is_some() {
2
} else {
0
};
let content_width = available_width.saturating_sub(border_width);
let content_height = available_height.saturating_sub(border_width);
let mut description_lines = split_string(&description, content_width as usize);
if description_lines.len() > content_height as usize {
description_lines.truncate(content_height as usize);
truncate_string_list(&mut description_lines, "...");
}
let content_width = description_lines
.iter()
.map(|s| s.width())
.max()
.unwrap_or_default()
.max(min_width.saturating_sub(border_width) as usize);
if let Some(border) = &self.default_details.border {
let horizontal_border = border.horizontal.to_string().repeat(content_width);
for line in &mut description_lines {
let padding = " ".repeat(content_width.saturating_sub(line.width()));
if use_ansi_coloring {
*line = format!(
"{}{}{}{}{}{}",
border.vertical,
self.settings.color.description_style.prefix(),
line,
padding,
RESET,
border.vertical
);
} else {
*line = format!("{}{}{}{}", border.vertical, line, padding, border.vertical);
}
}
description_lines.insert(
0,
format!(
"{}{}{}",
border.top_left, horizontal_border, border.top_right
),
);
description_lines.push(format!(
"{}{}{}",
border.bottom_left, horizontal_border, border.bottom_right
));
} else {
for line in &mut description_lines {
let padding = " ".repeat(content_width.saturating_sub(line.width()));
if use_ansi_coloring {
*line = format!(
"{}{}{}{}",
self.settings.color.description_style.prefix(),
line,
padding,
RESET
);
} else {
*line = format!("{line}{padding}");
}
}
}
description_lines
}
fn description_dims(
&self,
description: String,
max_width: u16,
max_height: u16,
min_width: u16,
) -> (u16, u16) {
let lines = self.create_description(description, false, max_width, max_height, min_width);
let height = lines.len() as u16;
let string = lines.first().cloned().unwrap_or_default();
let width = string.width() as u16;
(width, height)
}
fn create_value_string(
&self,
suggestion: &Suggestion,
index: usize,
use_ansi_coloring: bool,
padding: usize,
) -> String {
let border_width = if self.default_details.border.is_some() {
2
} else {
0
};
let vertical_border = self
.default_details
.border
.as_ref()
.map(|border| border.vertical)
.unwrap_or_default();
let display_value = suggestion.display_value();
let padding_right = (self.working_details.completion_width as usize)
.saturating_sub(self.display_widths[index] + border_width + padding);
let max_string_width =
(self.working_details.completion_width as usize).saturating_sub(border_width + padding);
let string = truncate_with_ansi(display_value, max_string_width);
if use_ansi_coloring {
let is_quote = |c: char| "`'\"".contains(c);
let shortest_base = &self.working_details.shortest_base_string;
let shortest_base = shortest_base
.strip_prefix(is_quote)
.unwrap_or(shortest_base);
let match_indices =
get_match_indices(display_value, &suggestion.match_indices, shortest_base);
let suggestion_style = suggestion.style.unwrap_or(self.settings.color.text_style);
let styled_string = if index == self.index() {
style_suggestion(
&string,
&match_indices,
&self.settings.color.selected_text_style,
&self.settings.color.selected_match_style,
None,
)
} else {
style_suggestion(
&string,
&match_indices,
&suggestion_style,
&self.settings.color.match_style,
None,
)
};
format!(
"{}{}{}{}{}{}{}",
vertical_border,
suggestion_style.prefix(),
" ".repeat(padding),
styled_string,
" ".repeat(padding_right),
RESET,
vertical_border,
)
} else {
let marker = if index == self.index() { ">" } else { "" };
format!(
"{}{}{}{}{}{}",
vertical_border,
" ".repeat(padding),
marker,
string,
" ".repeat(padding_right),
vertical_border,
)
}
}
}
impl Menu for IdeMenu {
fn settings(&self) -> &MenuSettings {
&self.settings
}
fn is_active(&self) -> bool {
self.active
}
fn can_quick_complete(&self) -> bool {
true
}
fn can_partially_complete(
&mut self,
values_updated: bool,
editor: &mut Editor,
completer: &mut dyn Completer,
) -> bool {
if !values_updated {
self.update_values(editor, completer);
}
if can_partially_complete(self.get_values(), editor) {
self.update_values(editor, completer);
true
} else {
false
}
}
fn menu_event(&mut self, event: MenuEvent) {
match &event {
MenuEvent::Activate(_) => self.active = true,
MenuEvent::Deactivate => {
self.active = false;
self.input = None;
}
_ => {}
}
self.event = Some(event);
}
fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) {
if self.settings.only_buffer_difference && self.input.is_none() {
self.input = Some(editor.get_buffer().to_string());
}
let (input, pos) = completer_input(
editor.get_buffer(),
editor.insertion_point(),
self.input.as_deref(),
self.settings.only_buffer_difference,
);
let (values, base_ranges) = completer.complete_with_base_ranges(&input, pos);
self.values = values;
self.display_widths = self
.values
.iter()
.map(|sugg| strip_ansi_escapes::strip_str(sugg.display_value()).width())
.collect();
self.working_details.shortest_base_string = base_ranges
.iter()
.map(|range| {
let end = floor_char_boundary(editor.get_buffer(), range.end);
let start = floor_char_boundary(editor.get_buffer(), range.start).min(end);
editor.get_buffer()[start..end].to_string()
})
.min_by_key(|s| s.width())
.unwrap_or_default();
self.longest_suggestion = *self.display_widths.iter().max().unwrap_or(&0);
self.reset_position();
}
fn update_working_details(
&mut self,
editor: &mut Editor,
completer: &mut dyn Completer,
painter: &Painter,
) {
if let Some(event) = self.event.take() {
match event {
MenuEvent::Activate(updated) => {
self.reset_position();
if !updated {
self.update_values(editor, completer);
}
}
MenuEvent::Deactivate => {}
MenuEvent::Edit(updated) => {
self.reset_position();
if !updated {
self.update_values(editor, completer);
}
}
MenuEvent::NextElement | MenuEvent::MoveDown => self.move_next(),
MenuEvent::PreviousElement | MenuEvent::MoveUp => self.move_previous(),
MenuEvent::MoveLeft
| MenuEvent::MoveRight
| MenuEvent::PreviousPage
| MenuEvent::NextPage => {}
}
let terminal_width = painter.screen_width();
let total_border_width = if self.default_details.border.is_some() {
2
} else {
0
};
let description = self.get_value().and_then(|v| {
let v = v.description?;
if v.is_empty() {
None
} else {
Some(v)
}
});
let mut available_lines = painter
.remaining_lines_real()
.min(self.default_details.max_completion_height);
if available_lines == 0 {
available_lines = painter.remaining_lines().min(self.min_rows());
}
let completion_width = ((self.longest_suggestion.min(u16::MAX as usize) as u16)
+ 2 * self.default_details.padding
+ total_border_width)
.min(self.default_details.max_completion_width)
.max(self.default_details.min_completion_width)
.max(3 + total_border_width); self.working_details.completion_width = completion_width;
let mut completion_pos = (self
.working_details
.cursor_col
.saturating_sub(total_border_width / 2)
as i16
+ self.default_details.cursor_offset)
.max(0) as u16;
if self.default_details.correct_cursor_pos {
let base_string = &self.working_details.shortest_base_string;
completion_pos = completion_pos.saturating_sub(base_string.width() as u16);
}
let mut completion_end = completion_pos.saturating_add(completion_width);
if completion_end > terminal_width {
let diff = completion_end - terminal_width;
completion_pos = completion_pos.saturating_sub(diff);
completion_end = terminal_width;
};
let all_space_left = completion_pos;
let all_space_right = terminal_width.saturating_sub(completion_end);
self.working_details.space_left = all_space_left;
self.working_details.space_right = all_space_right;
if let Some(description) = description {
let desc_space_left =
all_space_left.saturating_sub(self.default_details.description_offset);
let desc_space_right =
all_space_right.saturating_sub(self.default_details.description_offset);
self.working_details.description_is_right =
match self.default_details.description_mode {
DescriptionMode::Left => false,
DescriptionMode::Right => true,
DescriptionMode::PreferRight => {
desc_space_right >= self.default_details.min_description_width
}
};
let desc_space = if self.working_details.description_is_right {
desc_space_right
} else {
desc_space_left
};
self.working_details.description_width = self
.description_dims(
description,
desc_space.min(self.default_details.max_description_width),
available_lines,
self.default_details.min_description_width,
)
.0;
let max_offset = terminal_width
.saturating_sub(completion_width + self.working_details.description_width);
self.working_details.description_offset =
self.default_details.description_offset.min(max_offset);
if self.working_details.description_is_right {
self.working_details.space_right = all_space_right
.saturating_sub(self.working_details.description_width)
.saturating_sub(self.working_details.description_offset);
} else {
self.working_details.space_left = all_space_left
.saturating_sub(self.working_details.description_width)
.saturating_sub(self.working_details.description_offset);
}
} else {
self.working_details.description_width = 0;
self.working_details.description_offset = 0;
}
self.working_details.menu_width = completion_width
+ self.working_details.description_offset
+ self.working_details.description_width;
let visible_items = available_lines.saturating_sub(total_border_width);
self.skip_values = if self.selected <= self.skip_values {
self.selected
} else if self.selected >= self.skip_values + visible_items {
self.selected.saturating_sub(visible_items) + 1
} else {
self.skip_values
}
}
}
fn replace_in_buffer(&self, editor: &mut Editor) {
replace_in_buffer(self.get_value(), editor);
}
fn min_rows(&self) -> u16 {
self.get_rows()
}
fn get_values(&self) -> &[Suggestion] {
&self.values
}
fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
self.get_rows()
.min(self.default_details.max_completion_height)
}
fn menu_string(&self, available_lines: u16, use_ansi_coloring: bool) -> String {
if self.get_values().is_empty() {
self.no_records_msg(use_ansi_coloring)
} else {
let border_width = if self.default_details.border.is_some() {
2
} else {
0
};
let available_lines = available_lines.min(self.default_details.max_completion_height);
let skip_values = self.skip_values as usize;
let available_values = available_lines.saturating_sub(border_width) as usize;
let max_padding = self.working_details.completion_width.saturating_sub(
self.longest_suggestion.min(u16::MAX as usize) as u16 + border_width,
) / 2;
let corrected_padding = self.default_details.padding.min(max_padding) as usize;
let mut strings = self
.get_values()
.iter()
.skip(skip_values)
.take(available_values)
.enumerate()
.map(|(index, suggestion)| {
let index = index + skip_values;
self.create_value_string(
suggestion,
index,
use_ansi_coloring,
corrected_padding,
)
})
.collect::<Vec<String>>();
if let Some(border) = &self.default_details.border {
let inner_width = self.working_details.completion_width.saturating_sub(2) as usize;
strings.insert(
0,
format!(
"{}{}{}",
border.top_left,
border.horizontal.to_string().repeat(inner_width),
border.top_right,
),
);
strings.push(format!(
"{}{}{}",
border.bottom_left,
border.horizontal.to_string().repeat(inner_width),
border.bottom_right,
));
}
let description_height =
available_lines.min(self.default_details.max_description_height);
let description_lines = self
.get_value()
.and_then(|value| value.clone().description)
.map(|description| {
self.create_description(
description,
use_ansi_coloring,
self.working_details.description_width,
description_height,
self.working_details.description_width, )
})
.unwrap_or_default();
let distance_left = &" ".repeat(self.working_details.space_left as usize);
if self.working_details.description_is_right {
for (idx, pair) in strings
.clone()
.iter()
.zip_longest(description_lines.iter())
.enumerate()
{
match pair {
Both(_suggestion_line, description_line) => {
strings[idx] = format!(
"{}{}{}{}",
distance_left,
strings[idx],
" ".repeat(self.working_details.description_offset as usize),
description_line,
)
}
Left(suggestion_line) => {
strings[idx] = format!("{distance_left}{suggestion_line}");
}
Right(description_line) => strings.push(format!(
"{}{}",
" ".repeat(
(self.working_details.completion_width
+ self.working_details.description_offset)
as usize
) + distance_left,
description_line,
)),
}
}
} else {
for (idx, pair) in strings
.clone()
.iter()
.zip_longest(description_lines.iter())
.enumerate()
{
match pair {
Both(suggestion_line, description_line) => {
strings[idx] = format!(
"{}{}{}{}",
distance_left,
description_line,
" ".repeat(self.working_details.description_offset as usize),
suggestion_line,
)
}
Left(suggestion_line) => {
strings[idx] = format!(
"{}{}",
" ".repeat(
(self.working_details.description_width
+ self.working_details.description_offset)
as usize
) + distance_left,
suggestion_line,
);
}
Right(description_line) => {
strings.push(format!("{distance_left}{description_line}",))
}
}
}
}
strings.join("\r\n")
}
}
fn set_cursor_pos(&mut self, pos: (u16, u16)) {
self.working_details.cursor_col = pos.0;
}
}
fn split_string(input_str: &str, max_length: usize) -> Vec<String> {
let whitespace_split = input_str.split_whitespace();
let mut words = Vec::new();
for word in whitespace_split {
let word_len_cols = word.width();
if word_len_cols > max_length {
let mut width = 0;
let mut substring = String::new();
for grapheme in word.graphemes(true) {
let grapheme_width = grapheme.width();
if grapheme_width > max_length {
continue;
}
if width + grapheme_width > max_length {
words.push(substring);
substring = String::from(grapheme);
width = grapheme_width;
} else {
substring.push_str(grapheme);
width += grapheme_width;
}
}
if !substring.is_empty() {
words.push(substring);
}
} else {
words.push(word.to_string());
}
}
let mut result = Vec::new();
let mut string = String::new();
for word in words {
if string.width() + word.width() > max_length {
result.push(string.trim_end().to_string());
string = word;
string.push(' ');
} else {
string.push_str(&word);
string.push(' ');
}
}
if !string.trim_end().is_empty() {
result.push(string.trim_end().to_string());
}
result
}
fn truncate_string_list(list: &mut [String], truncation_chars: &str) {
let truncation_chars: Vec<char> = truncation_chars.chars().rev().collect();
let truncation_len = truncation_chars.len();
let mut to_replace = truncation_len;
'outer: for line in list.iter_mut().rev() {
let chars = UnicodeSegmentation::graphemes(line.as_str(), true).collect::<Vec<&str>>();
let mut new_line = String::new();
for grapheme in chars.into_iter().rev() {
if to_replace > 0 {
new_line.insert(0, truncation_chars[truncation_len - to_replace]);
to_replace -= 1;
} else {
new_line.insert_str(0, grapheme);
}
}
*line = new_line;
if to_replace == 0 {
break 'outer;
}
}
}
#[cfg(test)]
mod tests {
use crate::{Span, UndoBehavior};
use super::*;
use pretty_assertions::assert_eq;
use rstest::rstest;
#[rstest]
#[case(
"",
10,
vec![]
)]
#[case(
"description",
15,
vec![
"description".into(),
]
)]
#[case(
"this is a description",
10,
vec![
"this is a".into(),
"descriptio".into(),
"n".into(),
]
)]
#[case(
"this is another description",
2,
vec![
"th".into(),
"is".into(),
"is".into(),
"an".into(),
"ot".into(),
"he".into(),
"r".into(),
"de".into(),
"sc".into(),
"ri".into(),
"pt".into(),
"io".into(),
"n".into(),
]
)]
#[case(
"this is a description",
10,
vec![
"this is a".into(),
"descriptio".into(),
"n".into(),
]
)]
#[case(
"this is a description",
10,
vec![
"this is a".into(),
"descriptio".into(),
"n".into(),
]
)]
#[case(
"this is a description",
12,
vec![
"this is a".into(),
"description".into(),
]
)]
#[case(
"test",
1,
vec![
"t".into(),
"e".into(),
"s".into(),
"t".into(),
]
)]
#[case(
"😊a😊 😊bc de😊fg",
2,
vec![
"😊".into(),
"a".into(),
"😊".into(),
"😊".into(),
"bc".into(),
"de".into(),
"😊".into(),
"fg".into(),
]
)]
#[case(
"😊",
1,
vec![],
)]
#[case(
"t😊e😊s😊t",
1,
vec![
"t".into(),
"e".into(),
"s".into(),
"t".into(),
]
)]
fn test_split_string(
#[case] input: &str,
#[case] max_width: usize,
#[case] expected: Vec<String>,
) {
let result = split_string(input, max_width);
assert_eq!(result, expected)
}
#[rstest]
#[case(
&mut vec![
"this is a description".into(),
"that will be truncate".into(),
"d".into(),
],
"...",
vec![
"this is a description".into(),
"that will be trunca..".into(),
".".into(),
]
)]
#[case(
&mut vec![
"this is a description".into(),
"that will be truncate".into(),
"d".into(),
],
"....",
vec![
"this is a description".into(),
"that will be trunc...".into(),
".".into(),
]
)]
#[case(
&mut vec![
"😊a😊 😊bc de😊fg".into(),
"😊a😊 😊bc de😊fg".into(),
"😊a😊 😊bc de😊fg".into(),
],
"...",
vec![
"😊a😊 😊bc de😊fg".into(),
"😊a😊 😊bc de😊fg".into(),
"😊a😊 😊bc de...".into(),
]
)]
#[case(
&mut vec![
"t".into(),
"e".into(),
"s".into(),
"t".into(),
],
"..",
vec![
"t".into(),
"e".into(),
".".into(),
".".into(),
]
)]
#[case(
&mut vec![
"😊".into(),
"😊".into(),
"s".into(),
"t".into(),
],
"..😊",
vec![
"😊".into(),
".".into(),
".".into(),
"😊".into(),
]
)]
#[case(
&mut vec![
"".into(),
],
"test",
vec![
"".into()
],
)]
#[case(
&mut vec![
"t".into(),
"e".into(),
"s".into(),
"t".into()
],
"",
vec![
"t".into(),
"e".into(),
"s".into(),
"t".into()
],
)]
fn test_truncate_list_string(
#[case] input: &mut Vec<String>,
#[case] truncation_chars: &str,
#[case] expected: Vec<String>,
) {
truncate_string_list(input, truncation_chars);
assert_eq!(*input, expected)
}
macro_rules! partial_completion_tests {
(name: $test_group_name:ident, completions: $completions:expr, test_cases: $($name:ident: $value:expr,)*) => {
mod $test_group_name {
use crate::{menu::Menu, ColumnarMenu, core_editor::Editor, enums::UndoBehavior};
use super::FakeCompleter;
$(
#[test]
fn $name() {
let (input, expected) = $value;
let mut menu = ColumnarMenu::default();
let mut editor = Editor::default();
editor.set_buffer(input.to_string(), UndoBehavior::CreateUndoPoint);
let mut completer = FakeCompleter::new(&$completions);
menu.can_partially_complete(false, &mut editor, &mut completer);
assert_eq!(editor.get_buffer(), expected);
}
)*
}
}
}
partial_completion_tests! {
name: partial_completion_prefix_matches,
completions: ["build.rs", "build-all.sh"],
test_cases:
empty_completes_prefix: ("", "build"),
partial_completes_shared_prefix: ("bui", "build"),
full_prefix_completes_nothing: ("build", "build"),
}
partial_completion_tests! {
name: partial_completion_fuzzy_matches,
completions: ["build.rs", "build-all.sh", "prepare-build.sh"],
test_cases:
no_shared_prefix_completes_nothing: ("", ""),
shared_prefix_completes_nothing: ("bui", "bui"),
}
partial_completion_tests! {
name: partial_completion_fuzzy_same_prefix_matches,
completions: ["build.rs", "build-all.sh", "build-all-tests.sh"],
test_cases:
completes_no_shared_prefix: ("all", "all"),
}
struct FakeCompleter {
completions: Vec<String>,
}
impl FakeCompleter {
fn new(completions: &[&str]) -> Self {
Self {
completions: completions.iter().map(|c| c.to_string()).collect(),
}
}
}
impl Completer for FakeCompleter {
fn complete(&mut self, _line: &str, pos: usize) -> Vec<Suggestion> {
self.completions
.iter()
.map(|c| fake_suggestion(c, pos))
.collect()
}
}
fn fake_suggestion(name: &str, pos: usize) -> Suggestion {
Suggestion {
value: name.to_string(),
description: None,
style: None,
extra: None,
span: Span { start: 0, end: pos },
append_whitespace: false,
..Default::default()
}
}
#[test]
fn test_menu_replace_backtick() {
let mut completer = FakeCompleter::new(&["file1.txt", "file2.txt"]);
let mut menu = IdeMenu::default().with_name("testmenu");
let mut editor = Editor::default();
editor.set_buffer("file1.txt`".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
menu.replace_in_buffer(&mut editor);
assert!(
editor.is_cursor_at_buffer_end(),
"cursor should be at the end after completion"
);
}
#[test]
fn test_regression_panic_on_long_item() {
let commands = vec![
"hello world 2".into(),
"hello another very large option for hello word that will force one column".into(),
"this is the reedline crate".into(),
"abaaabas".into(),
"abaaacas".into(),
];
let mut completer = Box::new(crate::DefaultCompleter::new_with_wordlen(commands, 2));
let mut menu = IdeMenu::default().with_name("testmenu");
menu.working_details = IdeMenuDetails {
cursor_col: 50,
menu_width: 50,
completion_width: 50,
description_width: 50,
description_is_right: true,
space_left: 50,
space_right: 50,
description_offset: 50,
shortest_base_string: String::new(),
};
let mut editor = Editor::default();
editor.set_buffer(
"hello another very large option for hello word that will force one colu".to_string(),
UndoBehavior::CreateUndoPoint,
);
menu.update_values(&mut editor, &mut *completer);
menu.menu_string(500, true);
}
#[test]
fn test_menu_create_value_string() {
let mut completer = FakeCompleter::new(&["おはよう", "`おはよう(`"]);
let mut menu = IdeMenu::default().with_name("testmenu");
menu.working_details = IdeMenuDetails {
cursor_col: 50,
menu_width: 50,
completion_width: 50,
description_width: 50,
description_is_right: true,
space_left: 50,
space_right: 50,
description_offset: 50,
shortest_base_string: String::new(),
};
let mut editor = Editor::default();
editor.set_buffer("おは".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
assert!(menu.menu_string(2, true).contains("おは"));
}
#[test]
fn test_menu_create_value_string_starting_with_multibyte_char() {
let mut completer = FakeCompleter::new(&["验abc/"]);
let mut menu = IdeMenu::default().with_name("testmenu");
menu.working_details.completion_width = 50;
let mut editor = Editor::default();
editor.set_buffer("ac".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
assert!(menu.menu_string(10, true).contains("验"));
}
#[test]
fn test_menu_create_value_string_long_unicode_string() {
let mut completer = FakeCompleter::new(&[&("验".repeat(205) + "abc/")]);
let mut menu = IdeMenu::default().with_name("testmenu");
menu.working_details.completion_width = 50;
let mut editor = Editor::default();
editor.set_buffer("a".to_string(), UndoBehavior::CreateUndoPoint);
menu.update_values(&mut editor, &mut completer);
assert!(menu.menu_string(10, true).contains("验"));
}
}