ide style completions (#696)

* ide style completions

* descriptions

* truncate suggestion & description

* border width

* clippy & typos

* run cargo fmt

* add with_description_offset to builder

* fix empty description

* minimize description padding

* rework working details handling + fix CI

* add tests + change split function

* fix multiline prompt cursor pos
This commit is contained in:
maxomatic458 2024-01-18 17:37:06 +01:00 committed by GitHub
parent 2f3eb3e82f
commit 31eaeeb231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1569 additions and 15 deletions

View File

@ -6,5 +6,8 @@ extend-exclude = ["src/core_editor/line_buffer.rs"]
iterm = "iterm"
# For testing completion of the word build
bui = "bui"
# For testing string truncation
descriptio = "descriptio"
ot = "ot"
# for sqlite backed history
wheres = "wheres"

View File

@ -0,0 +1,58 @@
// Create a reedline object with tab completions support
// cargo run --example completions
//
// "t" [Tab] will allow you to select the completions "test" and "this is the reedline crate"
// [Enter] to select the chosen alternative
use reedline::{
default_emacs_keybindings, DefaultCompleter, DefaultPrompt, Emacs, IdeMenu, KeyCode,
KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, Signal,
};
use std::io;
fn add_menu_keybindings(keybindings: &mut Keybindings) {
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Tab,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::Menu("completion_menu".to_string()),
ReedlineEvent::MenuNext,
]),
);
}
fn main() -> io::Result<()> {
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 keybindings = default_emacs_keybindings();
add_menu_keybindings(&mut keybindings);
let edit_mode = Box::new(Emacs::new(keybindings));
let mut line_editor = Reedline::create()
.with_completer(completer)
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
.with_edit_mode(edit_mode);
let prompt = DefaultPrompt::default();
loop {
let sig = line_editor.read_line(&prompt)?;
match sig {
Signal::Success(buffer) => {
println!("We processed: {buffer}");
}
Signal::CtrlD | Signal::CtrlC => {
println!("\nAborted!");
break Ok(());
}
}
}
}

View File

@ -1688,7 +1688,7 @@ impl Reedline {
// Needs to add return carriage to newlines because when not in raw mode
// some OS don't fully return the carriage
let lines = PromptLines::new(
let mut lines = PromptLines::new(
prompt,
self.prompt_edit_mode(),
None,
@ -1700,6 +1700,11 @@ impl Reedline {
// Updating the working details of the active menu
for menu in self.menus.iter_mut() {
if menu.is_active() {
lines.prompt_indicator = menu.indicator().to_owned().into();
// If the menu requires the cursor position, update it (ide menu)
let cursor_pos = lines.cursor_pos(self.painter.screen_width());
menu.set_cursor_pos(cursor_pos);
menu.update_working_details(
&mut self.editor,
self.completer.as_mut(),

View File

@ -277,7 +277,7 @@ pub use validator::{DefaultValidator, ValidationResult, Validator};
mod menu;
pub use menu::{
menu_functions, ColumnarMenu, ListMenu, Menu, MenuEvent, MenuTextStyle, ReedlineMenu,
menu_functions, ColumnarMenu, IdeMenu, ListMenu, Menu, MenuEvent, MenuTextStyle, ReedlineMenu,
};
mod terminal_extensions;

View File

@ -728,6 +728,10 @@ 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)]

1465
src/menu/ide_menu.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -670,6 +670,10 @@ 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

@ -1,4 +1,5 @@
mod columnar_menu;
mod ide_menu;
mod list_menu;
pub mod menu_functions;
@ -6,6 +7,7 @@ use crate::core_editor::Editor;
use crate::History;
use crate::{completion::history::HistoryCompleter, painting::Painter, Completer, Suggestion};
pub use columnar_menu::ColumnarMenu;
pub use ide_menu::IdeMenu;
pub use list_menu::ListMenu;
use nu_ansi_term::{Color, Style};
@ -119,6 +121,8 @@ 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));
}
/// Allowed menus in Reedline
@ -312,4 +316,8 @@ impl Menu for ReedlineMenu {
fn get_values(&self) -> &[Suggestion] {
self.as_ref().get_values()
}
fn set_cursor_pos(&mut self, pos: (u16, u16)) {
self.as_mut().set_cursor_pos(pos);
}
}

View File

@ -271,17 +271,13 @@ impl Painter {
self.stdout
.queue(Print(&coerce_crlf(&lines.prompt_str_left)))?;
let prompt_indicator = match menu {
Some(menu) => menu.indicator(),
None => &lines.prompt_indicator,
};
if use_ansi_coloring {
self.stdout
.queue(SetForegroundColor(prompt.get_indicator_color()))?;
}
self.stdout.queue(Print(&coerce_crlf(prompt_indicator)))?;
self.stdout
.queue(Print(&coerce_crlf(&lines.prompt_indicator)))?;
if use_ansi_coloring {
self.stdout
@ -327,12 +323,7 @@ impl Painter {
// indicator is printed in the same line as the first line of the buffer
let prompt_lines = lines.prompt_lines_with_wrap(screen_width) as usize;
let prompt_indicator = match menu {
Some(menu) => menu.indicator(),
None => &lines.prompt_indicator,
};
let prompt_indicator_lines = prompt_indicator.lines().count();
let prompt_indicator_lines = &lines.prompt_indicator.lines().count();
let before_cursor_lines = lines.before_cursor.lines().count();
let total_lines_before = prompt_lines + prompt_indicator_lines + before_cursor_lines - 1;
@ -357,7 +348,7 @@ impl Painter {
// Adjusting extra_rows base on the calculated prompt line size
let extra_rows = extra_rows.saturating_sub(prompt_lines);
let indicator_skipped = skip_buffer_lines(prompt_indicator, extra_rows, None);
let indicator_skipped = skip_buffer_lines(&lines.prompt_indicator, extra_rows, None);
self.stdout.queue(Print(&coerce_crlf(indicator_skipped)))?;
if use_ansi_coloring {

View File

@ -88,6 +88,22 @@ impl<'prompt> PromptLines<'prompt> {
lines.saturating_sub(1) as u16
}
/// Calculate the cursor pos, based on the buffer and prompt.
/// The height is relative to the prompt
pub(crate) fn cursor_pos(&self, terminal_columns: u16) -> (u16, u16) {
// If we have a multiline prompt (e.g starship), we expect the cursor to be on the last line
let prompt_str = self.prompt_str_left.lines().last().unwrap_or_default();
let prompt_width = line_width(&format!("{}{}", prompt_str, self.prompt_indicator));
let buffer_width = line_width(&self.before_cursor);
let total_width = prompt_width + buffer_width;
let cursor_x = (total_width % terminal_columns as usize) as u16;
let cursor_y = (total_width / terminal_columns as usize) as u16;
(cursor_x, cursor_y)
}
/// Total lines that the prompt uses considering that it may wrap the screen
pub(crate) fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 {
let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator;