You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aichat/src/repl/init.rs

122 lines
3.8 KiB
Rust

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<Self> {
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<String> = 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<Box<FileBackedHistory>> {
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<char> = 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()
}