Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 46 additions & 57 deletions src/menu/columnar_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
core_editor::Editor,
menu_functions::{
can_partially_complete, completer_input, floor_char_boundary, get_match_indices,
replace_in_buffer, style_suggestion,
replace_in_buffer, style_suggestion, truncate_no_ansi,
},
painting::Painter,
Completer, Suggestion,
Expand Down Expand Up @@ -74,6 +74,8 @@ pub struct ColumnarMenu {
working_details: ColumnDetails,
/// Menu cached values
values: Vec<Suggestion>,
/// Cached display width of each suggestion in `values`
display_widths: Vec<usize>,
/// column position of the cursor. Starts from 0
col_pos: u16,
/// row position in the menu. Starts from 0
Expand All @@ -98,6 +100,7 @@ impl Default for ColumnarMenu {
min_rows: 3,
working_details: ColumnDetails::default(),
values: Vec::new(),
display_widths: Vec::new(),
col_pos: 0,
row_pos: 0,
skip_rows: 0,
Expand Down Expand Up @@ -381,7 +384,7 @@ impl ColumnarMenu {
use_ansi_coloring: bool,
) -> String {
let selected = index == self.index();
let empty_space = self.get_width().saturating_sub(suggestion.value.width());
let empty_space = self.get_width().saturating_sub(self.display_widths[index]);

if use_ansi_coloring {
// TODO(ysthakur): let the user strip quotes, rather than doing it here
Expand All @@ -394,53 +397,56 @@ impl ColumnarMenu {
let match_indices =
get_match_indices(&suggestion.value, &suggestion.match_indices, shortest_base);

let left_text_size = self.longest_suggestion + self.default_details.col_padding;
let left_text_size = self
.get_width()
.min(self.longest_suggestion + self.default_details.col_padding);
let description_size = self.get_width().saturating_sub(left_text_size);
let padding = left_text_size.saturating_sub(suggestion.value.len());
let padding = left_text_size.saturating_sub(self.display_widths[index]);

let value_style = if selected {
&self.settings.color.selected_text_style
} else {
&suggestion.style.unwrap_or(self.settings.color.text_style)
};
let value_trunc = truncate_no_ansi(&suggestion.value, left_text_size);
let styled_value = style_suggestion(
&value_style.paint(&suggestion.value).to_string(),
&value_style.paint(value_trunc).to_string(),
match_indices.as_ref(),
&self.settings.color.match_style,
);

if let Some(description) = &suggestion.description {
let desc_trunc = description
.chars()
.take(description_size)
.collect::<String>()
.replace('\n', " ");
if selected {
format!(
"{}{}{}{}{}",
styled_value,
value_style.prefix(),
" ".repeat(padding),
desc_trunc,
RESET,
)
} else {
match &suggestion.description {
Some(desc) if description_size > 3 => {
let desc = desc.replace('\n', "");
let desc_trunc = truncate_no_ansi(desc.as_str(), description_size);
if selected {
format!(
"{}{}{}{}{}",
styled_value,
value_style.prefix(),
" ".repeat(padding),
desc_trunc,
RESET,
)
} else {
format!(
"{}{}{}{}",
styled_value,
" ".repeat(padding),
self.settings.color.description_style.paint(desc_trunc),
RESET,
)
}
}
_ => {
format!(
"{}{}{}{}",
"{}{}{:>empty$}",
styled_value,
" ".repeat(padding),
self.settings.color.description_style.paint(desc_trunc),
RESET,
"",
empty = empty_space
)
}
} else {
format!(
"{}{}{:>empty$}",
styled_value,
RESET,
"",
empty = empty_space
)
}
} else {
// If no ansi coloring is found, then the selection word is the line in uppercase
Expand Down Expand Up @@ -548,6 +554,7 @@ impl Menu for ColumnarMenu {
let (values, base_ranges) = completer.complete_with_base_ranges(&input, pos);

self.values = values;
self.display_widths = self.values.iter().map(|sugg| sugg.value.width()).collect();
self.working_details.shortest_base_string = base_ranges
.iter()
.map(|range| {
Expand Down Expand Up @@ -614,43 +621,25 @@ impl Menu for ColumnarMenu {
.iter()
.any(|suggestion| suggestion.description.is_some());

let screen_width = painter.screen_width() as usize;
self.longest_suggestion = *self.display_widths.iter().max().unwrap_or(&0);
if exist_description {
self.working_details.columns = 1;
self.working_details.col_width = painter.screen_width() as usize;

self.longest_suggestion = self.get_values().iter().fold(0, |prev, suggestion| {
if prev >= suggestion.value.width() {
prev
} else {
suggestion.value.width()
}
});
self.working_details.col_width = screen_width;
} else {
let max_width = self.get_values().iter().fold(0, |acc, suggestion| {
let str_len = suggestion.value.width() + self.default_details.col_padding;
if str_len > acc {
str_len
} else {
acc
}
});

// If no default width is found, then the total screen width is used to estimate
// the column width based on the default number of columns
let default_width = if let Some(col_width) = self.default_details.col_width {
col_width
} else {
let col_width = painter.screen_width() / self.default_details.columns;
col_width as usize
screen_width / self.default_details.columns as usize
};

// Adjusting the working width of the column based the max line width found
// in the menu values
if max_width > default_width {
self.working_details.col_width = max_width;
} else {
self.working_details.col_width = default_width;
};
self.working_details.col_width = default_width
.max(self.longest_suggestion + self.default_details.col_padding)
.min(screen_width);

// The working columns is adjusted based on possible number of columns
// that could be fitted in the screen with the calculated column width
Expand Down
12 changes: 1 addition & 11 deletions src/menu/ide_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,17 +506,7 @@
let max_string_width =
(self.working_details.completion_width as usize).saturating_sub(border_width + padding);

let string = if suggestion.value.chars().count() > max_string_width {
let mut chars = suggestion
.value
.chars()
.take(max_string_width.saturating_sub(3))
.collect::<String>();
chars.push_str("...");
chars
} else {
suggestion.value.clone()
};
let string = truncate_no_ansi(&suggestion.value, max_string_width);

Check failure on line 509 in src/menu/ide_menu.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, basqlite)

cannot find function `truncate_no_ansi` in this scope

Check failure on line 509 in src/menu/ide_menu.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, default)

cannot find function `truncate_no_ansi` in this scope

Check failure on line 509 in src/menu/ide_menu.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, bashisms)

cannot find function `truncate_no_ansi` in this scope

Check failure on line 509 in src/menu/ide_menu.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, external_printer)

cannot find function `truncate_no_ansi` in this scope

Check failure on line 509 in src/menu/ide_menu.rs

View workflow job for this annotation

GitHub Actions / build-lint-test (ubuntu-latest, stable, sqlite)

cannot find function `truncate_no_ansi` in this scope
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the truncation helper here because the original code looks at number of chars rather than width


if use_ansi_coloring {
// TODO(ysthakur): let the user strip quotes, rather than doing it here
Expand Down
36 changes: 36 additions & 0 deletions src/menu/menu_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use itertools::{
};
use nu_ansi_term::{ansi::RESET, Style};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use crate::{Editor, Suggestion, UndoBehavior};

Expand Down Expand Up @@ -468,6 +469,31 @@ pub fn get_match_indices<'a>(
}
}

/// Truncate a string with no ANSI escapes to the given max width, which must be >=3.
///
/// If `s` is longer than `max_width`, the resulting string will end in "..."
/// and have width at most `max_width`.
pub(crate) fn truncate_no_ansi(s: &str, max_width: usize) -> Cow<str> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's named truncate_no_ansi because I intend to add a truncate_with_ansi helper that truncates strings that do contain ANSI escapes (so it must ignore the ANSI escapes)

if s.width() <= max_width {
Cow::Borrowed(s)
} else {
let trunc_suffix = "...";
let suffix_width = trunc_suffix.width();

let mut res = String::new();
let mut curr_width = 0;
for grapheme in s.graphemes(true) {
curr_width += grapheme.width();
if curr_width + suffix_width > max_width {
break;
}
res.push_str(grapheme);
}
res.push_str(trunc_suffix);
Cow::Owned(res)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -856,4 +882,14 @@ mod tests {
)
);
}

#[rstest]
#[case::shorter("asdf", 5, "asdf")]
// H has width 2
#[case::exact_width("asdH", 5, "asdH")]
#[case::one_longer("asdfH", 5, "as...")]
#[case::result_thinner_than_max("aHHH", 5, "a...")]
fn test_truncate(#[case] value: &str, #[case] max_width: usize, #[case] expected: &str) {
assert_eq!(expected, truncate_no_ansi(value, max_width));
}
}
Loading