mirror of
https://github.com/nushell/reedline.git
synced 2024-08-17 11:50:42 +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
|
// [Enter] to select the chosen alternative
|
||||||
|
|
||||||
use reedline::{
|
use reedline::{
|
||||||
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, Emacs, KeyCode,
|
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs,
|
||||||
KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
|
KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
@ -19,8 +19,21 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
|||||||
ReedlineEvent::MenuNext,
|
ReedlineEvent::MenuNext,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
keybindings.add_binding(
|
||||||
|
KeyModifiers::ALT,
|
||||||
|
KeyCode::Enter,
|
||||||
|
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
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![
|
let commands = vec![
|
||||||
"test".into(),
|
"test".into(),
|
||||||
"hello world".into(),
|
"hello world".into(),
|
||||||
@ -28,8 +41,15 @@ fn main() -> io::Result<()> {
|
|||||||
"this is the reedline crate".into(),
|
"this is the reedline crate".into(),
|
||||||
];
|
];
|
||||||
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2));
|
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2));
|
||||||
|
|
||||||
// Use the interactive menu to select options from the completer
|
// 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();
|
let mut keybindings = default_emacs_keybindings();
|
||||||
add_menu_keybindings(&mut keybindings);
|
add_menu_keybindings(&mut keybindings);
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
// [Enter] to select the chosen alternative
|
// [Enter] to select the chosen alternative
|
||||||
|
|
||||||
use reedline::{
|
use reedline::{
|
||||||
default_emacs_keybindings, DefaultCompleter, DefaultPrompt, Emacs, IdeMenu, KeyCode,
|
default_emacs_keybindings, DefaultCompleter, DefaultPrompt, DescriptionMode, EditCommand,
|
||||||
KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
|
Emacs, IdeMenu, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu,
|
||||||
|
Signal,
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
@ -19,8 +20,45 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
|||||||
ReedlineEvent::MenuNext,
|
ReedlineEvent::MenuNext,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
keybindings.add_binding(
|
||||||
|
KeyModifiers::ALT,
|
||||||
|
KeyCode::Enter,
|
||||||
|
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
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![
|
let commands = vec![
|
||||||
"test".into(),
|
"test".into(),
|
||||||
"hello world".into(),
|
"hello world".into(),
|
||||||
@ -28,8 +66,26 @@ fn main() -> io::Result<()> {
|
|||||||
"this is the reedline crate".into(),
|
"this is the reedline crate".into(),
|
||||||
];
|
];
|
||||||
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2));
|
let completer = Box::new(DefaultCompleter::new_with_wordlen(commands, 2));
|
||||||
|
|
||||||
// Use the interactive menu to select options from the completer
|
// 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();
|
let mut keybindings = default_emacs_keybindings();
|
||||||
add_menu_keybindings(&mut keybindings);
|
add_menu_keybindings(&mut keybindings);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
/// A span of source code, with positions in bytes
|
/// A span of source code, with positions in bytes
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
|
||||||
pub struct Span {
|
pub struct Span {
|
||||||
@ -31,6 +33,22 @@ pub trait Completer: Send {
|
|||||||
/// span to replace and the contents of that replacement
|
/// span to replace and the contents of that replacement
|
||||||
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion>;
|
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
|
/// 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
|
/// this command comes handy when trying to avoid to pull all the data at once
|
||||||
/// from the completer
|
/// from the completer
|
||||||
|
@ -356,10 +356,10 @@ impl CompletionNode {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
#[test]
|
#[test]
|
||||||
fn default_completer_with_non_ansi() {
|
fn default_completer_with_non_ansi() {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
let mut completions = DefaultCompleter::default();
|
let mut completions = DefaultCompleter::default();
|
||||||
completions.insert(
|
completions.insert(
|
||||||
["nushell", "null", "number"]
|
["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()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_pos(&mut self, _pos: (u16, u16)) {
|
|
||||||
// The columnar menu does not need the cursor position
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -49,11 +49,11 @@ impl Default for BorderSymbols {
|
|||||||
/// the initial declaration of the menu and are always kept as reference for the
|
/// the initial declaration of the menu and are always kept as reference for the
|
||||||
/// changeable [`IdeMenuDetails`] values.
|
/// changeable [`IdeMenuDetails`] values.
|
||||||
struct DefaultIdeMenuDetails {
|
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,
|
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,
|
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
|
/// this will be capped by the lines available in the terminal
|
||||||
pub max_completion_height: u16,
|
pub max_completion_height: u16,
|
||||||
/// Padding to the left and right of the suggestions
|
/// Padding to the left and right of the suggestions
|
||||||
@ -75,6 +75,14 @@ struct DefaultIdeMenuDetails {
|
|||||||
pub max_description_height: u16,
|
pub max_description_height: u16,
|
||||||
/// Offset from the suggestion box to the description box
|
/// Offset from the suggestion box to the description box
|
||||||
pub description_offset: u16,
|
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 {
|
impl Default for DefaultIdeMenuDetails {
|
||||||
@ -91,6 +99,7 @@ impl Default for DefaultIdeMenuDetails {
|
|||||||
max_description_width: 50,
|
max_description_width: 50,
|
||||||
max_description_height: 10,
|
max_description_height: 10,
|
||||||
description_offset: 1,
|
description_offset: 1,
|
||||||
|
correct_cursor_pos: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,6 +123,9 @@ struct IdeMenuDetails {
|
|||||||
pub space_right: u16,
|
pub space_right: u16,
|
||||||
/// Corrected description offset, based on the available space
|
/// Corrected description offset, based on the available space
|
||||||
pub description_offset: u16,
|
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
|
/// Menu to present suggestions like similar to Ide completion menus
|
||||||
@ -306,6 +318,13 @@ impl IdeMenu {
|
|||||||
self.default_details.description_offset = description_offset;
|
self.default_details.description_offset = description_offset;
|
||||||
self
|
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
|
// Menu functionality
|
||||||
@ -662,16 +681,16 @@ impl Menu for IdeMenu {
|
|||||||
|
|
||||||
/// Update menu values
|
/// Update menu values
|
||||||
fn update_values(&mut self, editor: &mut Editor, completer: &mut dyn Completer) {
|
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 {
|
if let Some(old_string) = &self.input {
|
||||||
let (start, input) = string_difference(editor.get_buffer(), old_string);
|
let (start, input) = string_difference(editor.get_buffer(), old_string);
|
||||||
if !input.is_empty() {
|
if !input.is_empty() {
|
||||||
completer.complete(input, start + input.len())
|
completer.complete_with_base_ranges(input, start + input.len())
|
||||||
} else {
|
} else {
|
||||||
completer.complete("", editor.insertion_point())
|
completer.complete_with_base_ranges("", editor.insertion_point())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
completer.complete("", editor.insertion_point())
|
completer.complete_with_base_ranges("", editor.insertion_point())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If there is a new line character in the line buffer, the completer
|
// 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
|
// Also, by replacing the new line character with a space, the insert
|
||||||
// position is maintain in the line buffer.
|
// position is maintain in the line buffer.
|
||||||
let trimmed_buffer = editor.get_buffer().replace("\r\n", " ").replace('\n', " ");
|
let trimmed_buffer = editor.get_buffer().replace("\r\n", " ").replace('\n', " ");
|
||||||
completer.complete(
|
completer.complete_with_base_ranges(
|
||||||
&trimmed_buffer[..editor.insertion_point()],
|
&trimmed_buffer[..editor.insertion_point()],
|
||||||
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();
|
self.reset_position();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -739,7 +764,18 @@ impl Menu for IdeMenu {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let terminal_width = painter.screen_width();
|
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() {
|
let border_width = if self.default_details.border.is_some() {
|
||||||
2
|
2
|
||||||
|
@ -670,10 +670,6 @@ impl Menu for ListMenu {
|
|||||||
fn min_rows(&self) -> u16 {
|
fn min_rows(&self) -> u16 {
|
||||||
self.max_lines + 1
|
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 {
|
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
|
/// Gets cached values from menu that will be displayed
|
||||||
fn get_values(&self) -> &[Suggestion];
|
fn get_values(&self) -> &[Suggestion];
|
||||||
/// Sets the position of the cursor (currently only required by the IDE menu)
|
/// 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
|
/// Allowed menus in Reedline
|
||||||
|
Loading…
Reference in New Issue
Block a user