From e528c09623df3bd953c3e1b385da0217f3076744 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 28 Aug 2023 16:14:55 -0400 Subject: [PATCH] Replace reedline with rustyline --- Cargo.lock | 165 ++++++++++-------------- Cargo.toml | 3 +- src/bin/dust.rs | 337 +++++++++++++++++++++--------------------------- 3 files changed, 213 insertions(+), 292 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22a464c..8809745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ checksum = "9eac0a7f2d7cd7a93b938af401d3d8e8b7094217989a7c25c55a953023436e31" dependencies = [ "accesskit", "accesskit_consumer", - "arrayvec 0.7.4", + "arrayvec", "once_cell", "paste", "windows 0.48.0", @@ -233,12 +233,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "arrayvec" version = "0.7.4" @@ -710,8 +704,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ab77dbd8adecaf3f0db40581631b995f312a8a5ae3aa9993188bb8f23d83a5b" dependencies = [ "crossterm", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "unicode-width", ] @@ -836,7 +830,6 @@ dependencies = [ "libc", "mio", "parking_lot", - "serde", "signal-hook", "signal-hook-mio", "winapi", @@ -936,11 +929,10 @@ dependencies = [ "egui_extras", "git2", "json", - "nu-ansi-term", "rand", "rayon", - "reedline", "reqwest", + "rustyline", "serde", "serde_json", "sysinfo", @@ -1065,6 +1057,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enumflags2" version = "0.7.7" @@ -1649,15 +1647,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.9" @@ -1948,6 +1937,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.24.3" @@ -2010,15 +2008,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nu-ansi-term" -version = "0.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -2365,6 +2354,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -2432,26 +2431,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "reedline" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fde955d11817fdcb79d703932fb6b473192cb36b6a92ba21f7f4ac0513374e" -dependencies = [ - "chrono", - "crossterm", - "fd-lock", - "itertools", - "nu-ansi-term", - "serde", - "strip-ansi-escapes", - "strum 0.25.0", - "strum_macros 0.25.2", - "thiserror", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "regex" version = "1.9.4" @@ -2557,6 +2536,41 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +[[package]] +name = "rustyline" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix 0.26.3", + "radix_trie", + "rustyline-derive", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "rustyline-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a32af5427251d2e4be14fc151eabe18abb4a7aad5efee7044da9f096c906a43" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "ryu" version = "1.0.15" @@ -2830,15 +2844,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -[[package]] -name = "strip-ansi-escapes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" -dependencies = [ - "vte", -] - [[package]] name = "strsim" version = "0.10.0" @@ -2851,12 +2856,6 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -[[package]] -name = "strum" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" - [[package]] name = "strum_macros" version = "0.24.3" @@ -2870,19 +2869,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "strum_macros" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.29", -] - [[package]] name = "syn" version = "1.0.109" @@ -2971,7 +2957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" dependencies = [ "arrayref", - "arrayvec 0.7.4", + "arrayvec", "bytemuck", "cfg-if", "png", @@ -3228,27 +3214,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "vte" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" -dependencies = [ - "arrayvec 0.5.2", - "utf8parse", - "vte_generate_state_changes", -] - -[[package]] -name = "vte_generate_state_changes" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" -dependencies = [ - "proc-macro2", - "quote", -] - [[package]] name = "waker-fn" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 75010e2..7a8b0d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,12 +27,11 @@ sysinfo = "0.29.6" toml = "0.7.6" toml_edit = "0.19.14" comfy-table = "7.0.1" -reedline = "0.22.0" clap = { version = "4.3.19", features = ["derive"] } -nu-ansi-term = "0.49" git2 = "0.17.2" csv = "1.2.2" json = "0.12.4" reqwest = { version = "0.11.18", features = ["blocking", "json"] } serde_json = "1.0.104" egui_extras = "0.22.0" +rustyline = { version = "12.0.0", features = ["with-file-history", "derive"] } diff --git a/src/bin/dust.rs b/src/bin/dust.rs index 6b0b975..b0d6689 100644 --- a/src/bin/dust.rs +++ b/src/bin/dust.rs @@ -1,13 +1,19 @@ //! Command line interface for the whale programming language. use clap::Parser; -use nu_ansi_term::{Color, Style}; -use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, DefaultHinter, DefaultPrompt, - DefaultPromptSegment, EditCommand, Emacs, FileBackedHistory, KeyCode, KeyModifiers, Reedline, - ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion, +use eframe::epaint::ahash::HashSet; +use rustyline::{ + completion::FilenameCompleter, + error::ReadlineError, + highlight::Highlighter, + hint::{Hint, Hinter, HistoryHinter}, + history::DefaultHistory, + validate::MatchingBracketValidator, + Completer, Context, DefaultEditor, Editor, Helper, Hinter, Validator, }; use std::{ + borrow::Cow, + collections::BTreeSet, fs::{self, read_to_string}, path::PathBuf, }; @@ -49,205 +55,156 @@ fn main() { } } +#[derive(Helper, Completer, Validator)] +struct DustReadline { + #[rustyline(Completer)] + completer: FilenameCompleter, + + tool_hints: Vec, + + #[rustyline(Validator)] + validator: MatchingBracketValidator, + + #[rustyline(Hinter)] + hinter: HistoryHinter, +} + +impl DustReadline { + fn new() -> Self { + Self { + completer: FilenameCompleter::new(), + validator: MatchingBracketValidator::new(), + hinter: HistoryHinter {}, + tool_hints: TOOL_LIST + .iter() + .map(|tool| ToolHint { + display: tool.info().identifier.to_string(), + complete_to: tool.info().identifier.len(), + }) + .collect(), + } + } +} + +struct ToolHint { + display: String, + complete_to: usize, +} + +impl Hint for ToolHint { + fn display(&self) -> &str { + &self.display + } + + fn completion(&self) -> Option<&str> { + if self.complete_to > 0 { + Some(&self.display[..self.complete_to]) + } else { + None + } + } +} + +impl ToolHint { + fn suffix(&self, strip_chars: usize) -> ToolHint { + ToolHint { + display: self.display[strip_chars..].to_string(), + complete_to: self.complete_to.saturating_sub(strip_chars), + } + } +} + +impl Hinter for DustReadline { + type Hint = ToolHint; + + fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { + if line.is_empty() || pos < line.len() { + return None; + } + + self.tool_hints.iter().find_map(|tool_hint| { + if tool_hint.display.starts_with(line) { + Some(tool_hint.suffix(pos)) + } else { + None + } + }) + } +} + +impl Highlighter for DustReadline { + fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> { + let _ = pos; + Cow::Borrowed(line) + } + + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + default: bool, + ) -> std::borrow::Cow<'b, str> { + let _ = default; + Cow::Borrowed(prompt) + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> { + Cow::Borrowed(hint) + } + + fn highlight_candidate<'c>( + &self, + candidate: &'c str, // FIXME should be Completer::Candidate + completion: rustyline::CompletionType, + ) -> std::borrow::Cow<'c, str> { + let _ = completion; + Cow::Borrowed(candidate) + } + + fn highlight_char(&self, line: &str, pos: usize) -> bool { + let _ = (line, pos); + false + } +} + fn run_cli_shell() { let mut context = VariableMap::new(); - let mut line_editor = setup_reedline(); - let prompt = DefaultPrompt { - left_prompt: DefaultPromptSegment::WorkingDirectory, - right_prompt: DefaultPromptSegment::CurrentDateTime, - }; + let mut rl: Editor = Editor::new().unwrap(); + + rl.set_helper(Some(DustReadline::new())); + + if rl.load_history("target/history.txt").is_err() { + println!("No previous history."); + } loop { - let sig = line_editor.read_line(&prompt); + let readline = rl.readline(">> "); + match readline { + Ok(line) => { + let line = line.as_str(); - match sig { - Ok(Signal::Success(buffer)) => { - let eval_result = eval_with_context(&buffer, &mut context); + rl.add_history_entry(line).unwrap(); + + let eval_result = eval_with_context(line, &mut context); match eval_result { Ok(value) => println!("{value}"), Err(error) => eprintln!("{error}"), } } - Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => { - println!("\nExit"); + Err(ReadlineError::Interrupted) => { + println!("CTRL-C"); break; } - signal => { - println!("Unhandled signal: {:?}", signal); + Err(ReadlineError::Eof) => { + println!("CTRL-D"); + break; + } + Err(err) => { + println!("Error: {:?}", err); + break; } } } -} - -struct WhaleCompeleter { - macro_list: Vec, - files: Vec, -} - -impl WhaleCompeleter { - pub fn new() -> Self { - WhaleCompeleter { - macro_list: Vec::new(), - files: Vec::new(), - } - } - - pub fn set_command_list(&mut self, macro_list: Vec<&'static dyn Tool>) -> &mut Self { - self.macro_list = macro_list - .iter() - .map(|r#macro| { - let ToolInfo { - identifier, - description, - group, - inputs, - } = r#macro.info(); - - let description = format!("{description} | {group}"); - let inputs = inputs - .iter() - .map(|value_type| value_type.to_string()) - .collect(); - - Suggestion { - value: identifier.to_string() + "()", - description: Some(description), - extra: Some(inputs), - ..Default::default() - } - }) - .collect(); - - self.macro_list - .sort_by_key(|suggestion| suggestion.extra.clone()); - - self - } - - pub fn get_suggestions(&mut self, start: usize, end: usize) -> Vec { - let macro_suggestions = self - .macro_list - .iter() - .cloned() - .map(|suggestion| Suggestion { - span: Span { start, end }, - ..suggestion - }); - let file_suggestions = self.files.iter().cloned().map(|suggestion| Suggestion { - span: Span { start, end }, - ..suggestion - }); - - file_suggestions.chain(macro_suggestions).collect() - } - - pub fn update_files(&mut self, mut path: &str) { - if path.starts_with('\"') { - path = &path[1..]; - } - - let path = PathBuf::from(path); - - if !path.is_dir() { - return; - } - - self.files = fs::read_dir(path) - .unwrap() - .map(|entry| { - let path = entry.unwrap().path(); - let path = path.to_string_lossy(); - - Suggestion { - value: format!("\"{path}\""), - description: None, - ..Default::default() - } - }) - .collect(); - } -} - -impl Completer for WhaleCompeleter { - fn complete(&mut self, line: &str, pos: usize) -> Vec { - let split = line.split(' '); - let current_word = split.last().unwrap_or(""); - let start = pos.saturating_sub(current_word.len()); - let end = line.len(); - - self.update_files(current_word); - self.get_suggestions(start, end) - } -} - -fn setup_reedline() -> Reedline { - let mut completer = Box::new(WhaleCompeleter::new()); - - completer.set_command_list(TOOL_LIST.to_vec()); - - let completion_menu = Box::new( - ColumnarMenu::default() - .with_name("completion_menu") - .with_columns(1) - .with_text_style(Style { - foreground: Some(Color::White), - is_dimmed: false, - ..Default::default() - }) - .with_description_text_style(Style { - is_dimmed: true, - ..Default::default() - }) - .with_selected_text_style(Style { - is_bold: true, - background: Some(Color::Black), - foreground: Some(Color::White), - ..Default::default() - }), - ); - - let mut keybindings = default_emacs_keybindings(); - keybindings.add_binding( - KeyModifiers::NONE, - KeyCode::Tab, - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::Menu("completion_menu".to_string()), - ReedlineEvent::MenuNext, - ]), - ); - keybindings.add_binding( - KeyModifiers::SHIFT, - KeyCode::Tab, - ReedlineEvent::UntilFound(vec![ - ReedlineEvent::Menu("completion_menu".to_string()), - ReedlineEvent::MenuPrevious, - ]), - ); - keybindings.add_binding( - KeyModifiers::ALT, - KeyCode::Enter, - ReedlineEvent::Edit(vec![EditCommand::InsertNewline]), - ); - - let edit_mode = Box::new(Emacs::new(keybindings)); - let history = Box::new( - FileBackedHistory::with_file(100, "target/history.txt".into()) - .expect("Error configuring shell history file."), - ); - let mut hinter = DefaultHinter::default(); - - hinter = hinter.with_style(Style { - foreground: Some(Color::Yellow), - ..Default::default() - }); - - Reedline::create() - .with_completer(completer) - .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) - .with_edit_mode(edit_mode) - .with_history(history) - .with_hinter(Box::new(hinter)) - .with_partial_completions(true) - .with_quick_completions(true) + + rl.save_history("target/history.txt").unwrap(); }