get back the ranges of the strings from the completer used for generating completions (#713)

* get correct cursor pos when menu indicator contains newline

* add tests

* fix cursor pos in multiline prompt

* make description mode enum public

* add doc comment

* respect windows newline in update_values

* Revert "respect windows newline in update_values"

This reverts commit 070d600545.

* add complete_with_base_ranges to Completer

* add builder for correct_cursor_pos

* add config options to completion examples
This commit is contained in:
maxomatic458 2024-01-22 22:08:52 +01:00 committed by GitHub
parent 32a391675d
commit cbb56e25d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 197 additions and 26 deletions

View File

@ -5,8 +5,8 @@
// [Enter] to select the chosen alternative
use reedline::{
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, Emacs, KeyCode,
KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs,
KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
};
use std::io;
@ -19,8 +19,21 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
ReedlineEvent::MenuNext,
]),
);
keybindings.add_binding(
KeyModifiers::ALT,
KeyCode::Enter,
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
);
}
fn main() -> io::Result<()> {
// Number of columns
let columns: u16 = 4;
// Column width
let col_width: Option<usize> = None;
// Column padding
let col_padding: usize = 2;
let commands = vec![
"test".into(),
"hello world".into(),
@ -28,8 +41,15 @@ fn main() -> io::Result<()> {
"this is the reedline crate".into(),
];
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2));
// Use the interactive menu to select options from the completer
let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu"));
let columnar_menu = ColumnarMenu::default()
.with_name("completion_menu")
.with_columns(columns)
.with_column_width(col_width)
.with_column_padding(col_padding);
let completion_menu = Box::new(columnar_menu);
let mut keybindings = default_emacs_keybindings();
add_menu_keybindings(&mut keybindings);

View File

@ -5,8 +5,9 @@
// [Enter] to select the chosen alternative
use reedline::{
default_emacs_keybindings, DefaultCompleter, DefaultPrompt, Emacs, IdeMenu, KeyCode,
KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand,
Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu,
Signal,
};
use std::io;
@ -19,8 +20,45 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
ReedlineEvent::MenuNext,
]),
);
keybindings.add_binding(
KeyModifiers::ALT,
KeyCode::Enter,
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
);
}
fn main() -> io::Result<()> {
// Min width of the completion box, including the border
let min_completion_width: u16 = 0;
// Max width of the completion box, including the border
let max_completion_width: u16 = 50;
// Max height of the completion box, including the border
let max_completion_height = u16::MAX;
// Padding inside of the completion box (on the left and right side)
let padding: u16 = 0;
// Whether to draw the default border around the completion box
let border: bool = false;
// Offset of the cursor from the top left corner of the completion box
// By default the top left corner is below the cursor
let cursor_offset: i16 = 0;
// How the description should be aligned
let description_mode: DescriptionMode = DescriptionMode::PreferRight;
// Min width of the description box, including the border
let min_description_width: u16 = 0;
// Max width of the description box, including the border
let max_description_width: u16 = 50;
// Distance between the completion and the description box
let description_offset: u16 = 1;
// If true, the cursor pos will be corrected, so the suggestions match up with the typed text
// ```text
// C:\> str
// str join
// str trim
// str split
// ```
// If a border is being used
let correct_cursor_pos: bool = false;
let commands = vec![
"test".into(),
"hello world".into(),
@ -28,8 +66,26 @@ fn main() -> io::Result<()> {
"this is the reedline crate".into(),
];
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2));
// Use the interactive menu to select options from the completer
let completion_menu = Box::new(IdeMenu::default().with_name("completion_menu"));
let mut ide_menu = IdeMenu::default()
.with_name("completion_menu")
.with_min_completion_width(min_completion_width)
.with_max_completion_width(max_completion_width)
.with_max_completion_height(max_completion_height)
.with_padding(padding)
.with_cursor_offset(cursor_offset)
.with_description_mode(description_mode)
.with_min_description_width(min_description_width)
.with_max_description_width(max_description_width)
.with_description_offset(description_offset)
.with_correct_cursor_pos(correct_cursor_pos);
if border {
ide_menu = ide_menu.with_default_border();
}
let completion_menu = Box::new(ide_menu);
let mut keybindings = default_emacs_keybindings();
add_menu_keybindings(&mut keybindings);

View File

@ -1,3 +1,5 @@
use std::ops::Range;
/// A span of source code, with positions in bytes
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct Span {
@ -31,6 +33,22 @@ pub trait Completer: Send {
/// span to replace and the contents of that replacement
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion>;
/// same as [`Completer::complete`] but it will return a vector of ranges of the strings
/// the suggestions are based on
fn complete_with_base_ranges(
&mut self,
line: &str,
pos: usize,
) -> (Vec<Suggestion>, Vec<Range<usize>>) {
let mut ranges = vec![];
let suggestions = self.complete(line, pos);
for suggestion in &suggestions {
ranges.push(suggestion.span.start..suggestion.span.end);
}
ranges.dedup();
(suggestions, ranges)
}
/// action that will return a partial section of available completions
/// this command comes handy when trying to avoid to pull all the data at once
/// from the completer

View File

@ -356,10 +356,10 @@ impl CompletionNode {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn default_completer_with_non_ansi() {
use super::*;
let mut completions = DefaultCompleter::default();
completions.insert(
["", "", ""]
@ -395,4 +395,51 @@ mod tests {
]
);
}
#[test]
fn default_completer_with_start_strings() {
let mut completions = DefaultCompleter::default();
completions.insert(
["this is the reedline crate", "test"]
.iter()
.map(|s| s.to_string())
.collect(),
);
let buffer = "this is t";
let (suggestions, ranges) = completions.complete_with_base_ranges(buffer, 9);
assert_eq!(
suggestions,
[
Suggestion {
value: "test".into(),
description: None,
extra: None,
span: Span { start: 8, end: 9 },
append_whitespace: false,
},
Suggestion {
value: "this is the reedline crate".into(),
description: None,
extra: None,
span: Span { start: 8, end: 9 },
append_whitespace: false,
},
Suggestion {
value: "this is the reedline crate".into(),
description: None,
extra: None,
span: Span { start: 0, end: 9 },
append_whitespace: false,
},
]
);
assert_eq!(ranges, [8..9, 0..9]);
assert_eq!(
["t", "this is t"],
[&buffer[ranges[0].clone()], &buffer[ranges[1].clone()]]
);
}
}

View File

@ -728,10 +728,6 @@ impl Menu for ColumnarMenu {
.collect()
}
}
fn set_cursor_pos(&mut self, _pos: (u16, u16)) {
// The columnar menu does not need the cursor position
}
}
#[cfg(test)]

View File

@ -49,11 +49,11 @@ impl Default for BorderSymbols {
/// the initial declaration of the menu and are always kept as reference for the
/// changeable [`IdeMenuDetails`] values.
struct DefaultIdeMenuDetails {
/// Minimum width of the completion box, including the border
/// Min width of the completion box, including the border
pub min_completion_width: u16,
/// max width of the completion box, including the border
/// Max width of the completion box, including the border
pub max_completion_width: u16,
/// max height of the completion box, including the border
/// Max height of the completion box, including the border
/// this will be capped by the lines available in the terminal
pub max_completion_height: u16,
/// Padding to the left and right of the suggestions
@ -75,6 +75,14 @@ struct DefaultIdeMenuDetails {
pub max_description_height: u16,
/// Offset from the suggestion box to the description box
pub description_offset: u16,
/// If true, the cursor pos will be corrected, so the suggestions match up with the typed text
/// ```text
/// C:\> str
/// str join
/// str trim
/// str split
/// ```
pub correct_cursor_pos: bool,
}
impl Default for DefaultIdeMenuDetails {
@ -91,6 +99,7 @@ impl Default for DefaultIdeMenuDetails {
max_description_width: 50,
max_description_height: 10,
description_offset: 1,
correct_cursor_pos: false,
}
}
}
@ -114,6 +123,9 @@ struct IdeMenuDetails {
pub space_right: u16,
/// Corrected description offset, based on the available space
pub description_offset: u16,
/// The ranges of the strings, the suggestions are based on (ranges in [`Editor::get_buffer`])
/// This is required to adjust the suggestion boxes position, when `correct_cursor_pos` in [`DefaultIdeMenuDetails`] is true
pub base_strings: Vec<String>,
}
/// Menu to present suggestions like similar to Ide completion menus
@ -306,6 +318,13 @@ impl IdeMenu {
self.default_details.description_offset = description_offset;
self
}
/// Menu builder with new correct cursor pos
#[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
}
}
// Menu functionality
@ -662,16 +681,16 @@ impl Menu for IdeMenu {
/// Update menu values
fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) {
self.values = if self.only_buffer_difference {
let (values, base_ranges) = if self.only_buffer_difference {
if let Some(old_string) = &self.input {
let (start, input) = string_difference(editor.get_buffer(), old_string);
if !input.is_empty() {
completer.complete(input, start + input.len())
completer.complete_with_base_ranges(input, start + input.len())
} else {
completer.complete("", editor.insertion_point())
completer.complete_with_base_ranges("", editor.insertion_point())
}
} else {
completer.complete("", editor.insertion_point())
completer.complete_with_base_ranges("", editor.insertion_point())
}
} else {
// If there is a new line character in the line buffer, the completer
@ -680,12 +699,18 @@ impl Menu for IdeMenu {
// Also, by replacing the new line character with a space, the insert
// position is maintain in the line buffer.
let trimmed_buffer = editor.get_buffer().replace("\r\n", " ").replace('\n', " ");
completer.complete(
completer.complete_with_base_ranges(
&trimmed_buffer[..editor.insertion_point()],
editor.insertion_point(),
)
};
self.values = values;
self.working_details.base_strings = base_ranges
.iter()
.map(|range| editor.get_buffer()[range.clone()].to_string())
.collect::<Vec<String>>();
self.reset_position();
}
@ -739,7 +764,18 @@ impl Menu for IdeMenu {
});
let terminal_width = painter.screen_width();
let cursor_pos = self.working_details.cursor_col;
let mut cursor_pos = self.working_details.cursor_col;
if self.default_details.correct_cursor_pos {
let base_string = self
.working_details
.base_strings
.iter()
.min_by_key(|s| s.len())
.cloned()
.unwrap_or_default();
cursor_pos = cursor_pos.saturating_sub(base_string.width() as u16);
}
let border_width = if self.default_details.border.is_some() {
2

View File

@ -670,10 +670,6 @@ impl Menu for ListMenu {
fn min_rows(&self) -> u16 {
self.max_lines + 1
}
fn set_cursor_pos(&mut self, _pos: (u16, u16)) {
// The list menu does not need the cursor position
}
}
fn number_of_lines(entry: &str, max_lines: usize, terminal_columns: u16) -> u16 {

View File

@ -123,7 +123,9 @@ pub trait Menu: Send {
/// Gets cached values from menu that will be displayed
fn get_values(&self) -> &[Suggestion];
/// Sets the position of the cursor (currently only required by the IDE menu)
fn set_cursor_pos(&mut self, pos: (u16, u16));
fn set_cursor_pos(&mut self, _pos: (u16, u16)) {
// empty implementation to make it optional
}
}
/// Allowed menus in Reedline