Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 11 commits into from
Jan 22, 2024
26 changes: 23 additions & 3 deletions examples/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,17 +19,37 @@ 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(),
"hello world reedline".into(),
"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);
Expand Down
62 changes: 59 additions & 3 deletions examples/ide_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,17 +20,72 @@ 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(),
"hello world reedline".into(),
"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);
Expand Down
18 changes: 18 additions & 0 deletions src/completion/base.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
Expand Down
51 changes: 49 additions & 2 deletions src/completion/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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()]]
);
}
}
4 changes: 0 additions & 4 deletions src/menu/columnar_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
54 changes: 45 additions & 9 deletions src/menu/ide_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -91,6 +99,7 @@ impl Default for DefaultIdeMenuDetails {
max_description_width: 50,
max_description_height: 10,
description_offset: 1,
correct_cursor_pos: false,
}
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

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

The expanded example with the added ability to throw some new lines in there will test this part of the code better

&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();
}

Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions src/menu/list_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/menu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down