Implement reedline crate with highlighting

This commit is contained in:
Jeff 2024-01-25 01:28:22 -05:00
parent 12f82f7bfd
commit ac29f0210f
4 changed files with 410 additions and 148 deletions

306
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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",

View File

@ -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<ToolHint>,
#[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<Self::Hint> {
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<DustReadline, DefaultHistory> =
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(())
}