use super::Repl; use crate::config::{Config, SharedConfig}; use anyhow::{Context, Result}; use reedline::{ default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, DefaultPromptSegment, Emacs, FileBackedHistory, KeyCode, KeyModifiers, Keybindings, Reedline, ReedlineEvent, ReedlineMenu, ValidationResult, Validator, }; use super::REPL_COMMANDS; const MENU_NAME: &str = "completion_menu"; impl Repl { pub fn init(config: SharedConfig) -> Result { let multiline_commands: Vec<&'static str> = REPL_COMMANDS .iter() .filter(|(_, _, v)| *v) .map(|(v, _, _)| *v) .collect(); let completer = Self::create_completer(config); let keybindings = Self::create_keybindings(); let history = Self::create_history()?; let menu = Self::create_menu(); let edit_mode = Box::new(Emacs::new(keybindings)); let editor = Reedline::create() .with_completer(Box::new(completer)) .with_history(history) .with_menu(menu) .with_edit_mode(edit_mode) .with_quick_completions(true) .with_partial_completions(true) .with_validator(Box::new(ReplValidator { multiline_commands })) .with_ansi_colors(true); let prompt = Self::create_prompt(); Ok(Self { editor, prompt }) } fn create_prompt() -> DefaultPrompt { DefaultPrompt::new(DefaultPromptSegment::Empty, DefaultPromptSegment::Empty) } fn create_completer(config: SharedConfig) -> DefaultCompleter { let mut completion: Vec = REPL_COMMANDS .into_iter() .map(|(v, _, _)| v.to_string()) .collect(); completion.extend(config.borrow().repl_completions()); let mut completer = DefaultCompleter::with_inclusions(&['.', '-', '_']).set_min_word_len(2); completer.insert(completion.clone()); completer } fn create_keybindings() -> Keybindings { let mut keybindings = default_emacs_keybindings(); keybindings.add_binding( KeyModifiers::NONE, KeyCode::Tab, ReedlineEvent::UntilFound(vec![ ReedlineEvent::Menu(MENU_NAME.to_string()), ReedlineEvent::MenuNext, ]), ); keybindings.add_binding( KeyModifiers::CONTROL, KeyCode::Char('l'), ReedlineEvent::ExecuteHostCommand(".clear screen".into()), ); keybindings } fn create_menu() -> ReedlineMenu { let completion_menu = ColumnarMenu::default().with_name(MENU_NAME); ReedlineMenu::EngineCompleter(Box::new(completion_menu)) } fn create_history() -> Result> { Ok(Box::new( FileBackedHistory::with_file(1000, Config::history_file()?) .with_context(|| "Failed to setup history file")?, )) } } struct ReplValidator { multiline_commands: Vec<&'static str>, } impl Validator for ReplValidator { fn validate(&self, line: &str) -> ValidationResult { if line.split('"').count() % 2 == 0 || incomplete_brackets(line, &self.multiline_commands) { ValidationResult::Incomplete } else { ValidationResult::Complete } } } fn incomplete_brackets(line: &str, multiline_commands: &[&str]) -> bool { let mut balance: Vec = Vec::new(); let line = line.trim_start(); if !multiline_commands.iter().any(|v| line.starts_with(v)) { return false; } for c in line.chars() { if c == '{' { balance.push('}'); } else if c == '}' { if let Some(last) = balance.last() { if last == &c { balance.pop(); } } } } !balance.is_empty() }