mirror of
https://github.com/nushell/reedline.git
synced 2024-08-17 11:50:42 +03:00
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:
parent
2f3eb3e82f
commit
31eaeeb231
@ -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"
|
||||
|
58
examples/ide_completions.rs
Normal file
58
examples/ide_completions.rs
Normal 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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(),
|
||||
|
@ -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;
|
||||
|
@ -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
1465
src/menu/ide_menu.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user