From ac29f0210f4fd3a7efe03ca666002f49d12597a9 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 25 Jan 2024 01:28:22 -0500 Subject: [PATCH] Implement reedline crate with highlighting --- Cargo.lock | 306 +++++++++++++++++++++++++++- Cargo.toml | 4 +- src/abstract_tree/built_in_value.rs | 2 +- src/main.rs | 246 ++++++++++------------ 4 files changed, 410 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f213897..002a18a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,6 +130,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-activity" version = "0.4.3" @@ -155,12 +161,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" [[package]] -name = "ansi_term" -version = "0.12.1" +name = "android-tzdata" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ - "winapi", + "libc", ] [[package]] @@ -217,7 +229,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" dependencies = [ - "clipboard-win", + "clipboard-win 4.5.0", "log", "objc", "objc-foundation", @@ -640,6 +652,19 @@ dependencies = [ "libc", ] +[[package]] +name = "chrono" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.0", +] + [[package]] name = "clap" version = "4.4.12" @@ -680,6 +705,28 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win 2.2.0", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -841,6 +888,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -939,9 +1012,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" name = "dust-lang" version = "0.4.1" dependencies = [ - "ansi_term", "cc", "clap", + "crossterm", "csv", "eframe", "egui", @@ -951,8 +1024,10 @@ dependencies = [ "getrandom", "libc", "log", + "nu-ansi-term", "rand", "rayon", + "reedline", "reqwest", "rustyline", "serde", @@ -1264,6 +1339,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.9.0" @@ -1566,6 +1653,19 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -1671,6 +1771,29 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1745,6 +1868,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -1851,6 +1983,17 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "libsqlite3-sys" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2067,6 +2210,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[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" @@ -2533,6 +2685,29 @@ dependencies = [ "thiserror", ] +[[package]] +name = "reedline" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f4e89a0f80909b3ca4bca9759ed37e4bfddb6f5d2ffb1b4ceb2b1638a3e1eb" +dependencies = [ + "chrono", + "clipboard", + "crossterm", + "fd-lock", + "itertools", + "nu-ansi-term", + "rusqlite", + "serde", + "serde_json", + "strip-ansi-escapes", + "strum", + "strum_macros", + "thiserror", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "regex" version = "1.10.2" @@ -2612,6 +2787,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rusqlite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" +dependencies = [ + "bitflags 2.4.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2645,6 +2834,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "rustyline" version = "12.0.0" @@ -2653,7 +2848,7 @@ checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" dependencies = [ "bitflags 2.4.1", "cfg-if", - "clipboard-win", + "clipboard-win 4.5.0", "fd-lock", "home", "libc", @@ -2813,6 +3008,27 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2913,12 +3129,40 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[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.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.44", +] + [[package]] name = "syn" version = "1.0.109" @@ -3257,6 +3501,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "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.1" @@ -3527,6 +3791,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-implement" version = "0.48.0" @@ -3800,6 +4073,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -3833,6 +4115,16 @@ dependencies = [ "nix 0.26.4", ] +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] + [[package]] name = "xcursor" version = "0.3.5" diff --git a/Cargo.toml b/Cargo.toml index b331803..d18db75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ opt-level = 1 opt-level = 3 [dependencies] -ansi_term = "0.12.1" clap = { version = "4.4.4", features = ["derive"] } csv = "1.2.2" egui = "0.24.1" @@ -40,6 +39,9 @@ tree-sitter = "0.20.10" egui_extras = "0.24.2" enum-iterator = "1.4.1" env_logger = "0.10" +reedline = { version = "0.28.0", features = ["clipboard", "sqlite"] } +crossterm = "0.27.0" +nu-ansi-term = "0.49.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] env_logger = "0.10" diff --git a/src/abstract_tree/built_in_value.rs b/src/abstract_tree/built_in_value.rs index 33e921a..169e702 100644 --- a/src/abstract_tree/built_in_value.rs +++ b/src/abstract_tree/built_in_value.rs @@ -31,7 +31,7 @@ pub enum BuiltInValue { } impl BuiltInValue { - pub fn name(&self) -> &'static str { + pub const fn name(&self) -> &'static str { match self { BuiltInValue::Args => "args", BuiltInValue::AssertEqual => "assert_equal", diff --git a/src/main.rs b/src/main.rs index 73fdc81..0d3fed0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,16 @@ //! Command line interface for the dust programming language. use clap::{Parser, Subcommand}; -use rustyline::{ - completion::FilenameCompleter, - config::Builder, - error::ReadlineError, - highlight::Highlighter, - hint::{Hint, Hinter, HistoryHinter}, - history::DefaultHistory, - ColorMode, Completer, CompletionType, Context, Editor, Helper, Validator, +use crossterm::event::{KeyCode, KeyModifiers}; +use nu_ansi_term::Style; +use reedline::{ + default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultPrompt, EditCommand, Emacs, + Highlighter, Reedline, ReedlineEvent, ReedlineMenu, Signal, SqliteBackedHistory, StyledText, }; -use std::{borrow::Cow, fs::read_to_string}; +use std::{fs::read_to_string, path::PathBuf}; -use dust_lang::{built_in_values, Interpreter, Map, Value}; +use dust_lang::{built_in_values, Interpreter, Map, Result, Value}; /// Command-line arguments to be parsed. #[derive(Parser, Debug)] @@ -69,7 +66,14 @@ fn main() { } if args.path.is_none() && args.command.is_none() { - return run_cli_shell(context); + let run_shell_result = run_shell(context); + + match run_shell_result { + Ok(_) => {} + Err(error) => eprintln!("{error}"), + } + + return; } let source = if let Some(path) = &args.path { @@ -108,157 +112,121 @@ fn main() { } } -#[derive(Helper, Completer, Validator)] -struct DustReadline { - #[rustyline(Completer)] - completer: FilenameCompleter, - - hints: Vec, - - #[rustyline(Hinter)] - _hinter: HistoryHinter, +struct DustHighlighter { + context: Map, } -impl DustReadline { - fn new() -> Self { - let mut hints = Vec::new(); +impl DustHighlighter { + fn new(context: Map) -> Self { + Self { context } + } +} - for built_in_value in built_in_values() { - let mut display = built_in_value.name().to_string(); +impl Highlighter for DustHighlighter { + fn highlight(&self, line: &str, _cursor: usize) -> reedline::StyledText { + fn highlight_identifier(styled: &mut StyledText, word: &str, map: &Map) -> bool { + for (key, (value, _type)) in map.variables().unwrap().iter() { + if key == &word[0..word.len() - 1] { + styled.push((Style::new().bold(), word.to_string())); - if built_in_value.r#type().is_function() { - display.push_str("()"); - } + return true; + } - if built_in_value.r#type().is_map() { - let value = built_in_value.get(); - - if let Value::Map(map) = value { - for (key, (value, _)) in map.variables().unwrap().iter() { - let display = if value.is_function() { - format!("{display}:{key}()") - } else { - format!("{display}:{key}") - }; - - hints.push(ToolHint { - complete_to: display.len(), - display, - }) - } + if let Value::Map(nested_map) = value { + return highlight_identifier(styled, key, nested_map); } } - hints.push(ToolHint { - complete_to: display.len(), - display, - }) - } - - hints.push(ToolHint { - display: "output".to_string(), - complete_to: 0, - }); - - Self { - completer: FilenameCompleter::new(), - _hinter: HistoryHinter {}, - hints, - } - } -} - -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.hints.iter().find_map(|tool_hint| { - if tool_hint.display.starts_with(line) { - Some(tool_hint.suffix(pos)) - } else { - None + for built_in_value in built_in_values() { + if built_in_value.name() == &word[0..word.len() - 1] { + styled.push((Style::new().bold(), word.to_string())); + } } - }) + + false + } + + let mut styled = StyledText::new(); + + for word in line.split_inclusive(&[' ', ':', '(', ')', '{', '}', '[', ']']) { + let word_is_highlighted = highlight_identifier(&mut styled, word, &self.context); + + if !word_is_highlighted { + styled.push((Style::new(), word.to_string())); + } + } + + styled } } -impl Highlighter for DustReadline { - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - let highlighted = ansi_term::Colour::Yellow.paint(hint).to_string(); +fn run_shell(context: Map) -> Result<()> { + let mut interpreter = Interpreter::new(context.clone()); + let prompt = DefaultPrompt::default(); + let mut keybindings = default_emacs_keybindings(); - Cow::Owned(highlighted) + keybindings.add_binding( + KeyModifiers::ALT, + KeyCode::Char('m'), + ReedlineEvent::Edit(vec![EditCommand::BackspaceWord]), + ); + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); + + let edit_mode = Box::new(Emacs::new(keybindings)); + let history = Box::new( + SqliteBackedHistory::with_file(PathBuf::from("target/history"), None, None) + .expect("Error loading history."), + ); + let mut commands = Vec::new(); + + for built_in_value in built_in_values() { + commands.push(built_in_value.name().to_string()); } -} -fn run_cli_shell(context: Map) { - let mut interpreter = Interpreter::new(context); - let config = Builder::new() - .color_mode(ColorMode::Enabled) - .completion_type(CompletionType::List) - .build(); - let mut rl: Editor = - Editor::with_config(config).expect("Line editor could not be configured properly."); - - rl.set_helper(Some(DustReadline::new())); - - if rl.load_history("target/history.txt").is_err() { - println!("No previous history."); - } + let completer = Box::new(DefaultCompleter::new_with_wordlen(commands.clone(), 0)); + let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); + let mut line_editor = Reedline::create() + .with_edit_mode(edit_mode) + .with_history(history) + .with_highlighter(Box::new(DustHighlighter::new(context))) + .with_completer(completer) + .with_menu(ReedlineMenu::EngineCompleter(completion_menu)); loop { - let readline = rl.readline("* "); - match readline { - Ok(line) => { - let input = line.to_string(); + let sig = line_editor.read_line(&prompt); + match sig { + Ok(Signal::Success(buffer)) => { + if buffer.is_empty() { + continue; + } - rl.add_history_entry(line).unwrap(); + let run_result = interpreter.run(&buffer); - let eval_result = interpreter.run(&input); - - match eval_result { - Ok(value) => println!("{value}"), - Err(error) => { - eprintln!("{error}") + match run_result { + Ok(value) => { + if !value.is_none() { + println!("{value}") + } } + Err(error) => println!("Error: {error}"), } } - Err(ReadlineError::Interrupted) => break, - Err(ReadlineError::Eof) => break, - Err(error) => eprintln!("{error}"), + Ok(Signal::CtrlD) | Ok(Signal::CtrlC) => { + println!("\nAborted!"); + break; + } + x => { + println!("Event: {:?}", x); + } } } - rl.save_history("target/history.txt").unwrap(); + Ok(()) }