mirror of
https://github.com/nushell/reedline.git
synced 2024-07-14 19:00:30 +03:00
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:
parent
32a391675d
commit
cbb56e25d0
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
["nushell", "null", "number"]
|
||||
@ -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()]]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user