-
Notifications
You must be signed in to change notification settings - Fork 26
Implement proper REPL #515
base: master
Are you sure you want to change the base?
Changes from all commits
9e142c2
2dc7c03
19260f4
0e4b631
3546cb1
1f9b016
3519199
2534d5c
8ec93b2
5ffa222
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "saltwater-repl" | ||
version = "0.11.0" | ||
authors = ["Justus K <[email protected]>"] | ||
edition = "2018" | ||
|
||
[dependencies] | ||
saltwater-codegen = { path = "../saltwater-codegen", features = ["jit"] } | ||
saltwater-parser = { path = "../saltwater-parser" } | ||
rustyline = "6.2" | ||
rustyline-derive = "0.3" | ||
dirs-next = "1.0" | ||
owo-colors = "1.1" | ||
hex = "0.4.2" | ||
|
||
[[bin]] | ||
name = "swcci" | ||
path = "main.rs" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
use crate::repl::Repl; | ||
use std::fmt::Write; | ||
|
||
pub struct Command { | ||
pub names: &'static [&'static str], | ||
pub description: &'static str, | ||
pub action: fn(&mut Repl, &str), | ||
} | ||
|
||
pub fn default_commands() -> Vec<Command> { | ||
vec![ | ||
Command { | ||
names: &["help", "h"], | ||
description: "Shows this help message", | ||
action: help_command, | ||
}, | ||
Command { | ||
names: &["quit", "q"], | ||
description: "Quits the repl", | ||
action: quit_command, | ||
}, | ||
] | ||
} | ||
|
||
pub fn generate_help_message(cmds: &[Command]) -> String { | ||
let inner = || { | ||
let mut buf = String::new(); | ||
writeln!(buf, "Available commands:")?; | ||
for cmd in cmds { | ||
let names = cmd.names.iter().copied().collect::<Vec<_>>().join("|"); | ||
writeln!( | ||
buf, | ||
"{:>4}{}{:>4}{}", | ||
crate::repl::PREFIX, | ||
names, | ||
"", | ||
cmd.description | ||
)?; | ||
} | ||
Ok::<String, std::fmt::Error>(buf) | ||
}; | ||
inner().expect("failed to generate help message") | ||
} | ||
|
||
fn help_command(repl: &mut Repl, _args: &str) { | ||
println!("{}", repl.help_message); | ||
} | ||
|
||
fn quit_command(repl: &mut Repl, _args: &str) { | ||
Stupremee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
repl.save_history(); | ||
std::process::exit(0) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
use crate::repl::PREFIX; | ||
use owo_colors::OwoColorize; | ||
use rustyline::{ | ||
completion::{extract_word, Candidate, Completer}, | ||
highlight::{Highlighter, MatchingBracketHighlighter}, | ||
hint::Hinter, | ||
validate::{ValidationContext, ValidationResult, Validator}, | ||
Context, | ||
}; | ||
use rustyline_derive::Helper; | ||
use std::borrow::Cow; | ||
|
||
#[derive(Helper)] | ||
pub struct ReplHelper { | ||
highlighter: MatchingBracketHighlighter, | ||
commands: Vec<&'static str>, | ||
} | ||
|
||
impl ReplHelper { | ||
pub fn new(commands: Vec<&'static str>) -> Self { | ||
Self { | ||
commands, | ||
highlighter: Default::default(), | ||
} | ||
} | ||
} | ||
|
||
impl Highlighter for ReplHelper { | ||
fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { | ||
// TODO: Syntax highlighting. | ||
self.highlighter.highlight(line, pos) | ||
} | ||
|
||
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { | ||
Cow::Owned(hint.dimmed().to_string()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a shame this requires a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea it is. It would save so many allocations |
||
} | ||
|
||
fn highlight_char(&self, line: &str, pos: usize) -> bool { | ||
self.highlighter.highlight_char(line, pos) | ||
} | ||
} | ||
|
||
impl Validator for ReplHelper { | ||
fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> { | ||
if ctx.input().starts_with(crate::repl::PREFIX) { | ||
if self.commands.contains(&&ctx.input()[1..]) { | ||
return Ok(ValidationResult::Valid(None)); | ||
} else { | ||
return Ok(ValidationResult::Invalid(None)); | ||
} | ||
} | ||
|
||
let result = crate::repl::analyze_expr(ctx.input().to_string()); | ||
if let Err(err) = result { | ||
Ok(ValidationResult::Invalid(Some(err.data.to_string()))) | ||
} else { | ||
Ok(ValidationResult::Valid(None)) | ||
} | ||
} | ||
} | ||
|
||
impl Hinter for ReplHelper { | ||
fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option<String> { | ||
let start = &line[..pos]; | ||
if !start.starts_with(PREFIX) { | ||
return None; | ||
} | ||
let start = &start[PREFIX.len_utf8()..]; | ||
self.commands | ||
.iter() | ||
.find(|cmd| cmd.starts_with(start)) | ||
.map(|hint| String::from(&hint[start.len()..])) | ||
} | ||
} | ||
|
||
/// Wrapper around a `&'static str` to be used for completion candidates. | ||
pub struct CompletionCandidate { | ||
display: &'static str, | ||
} | ||
|
||
impl Candidate for CompletionCandidate { | ||
fn display(&self) -> &str { | ||
self.display | ||
} | ||
|
||
fn replacement(&self) -> &str { | ||
self.display | ||
} | ||
} | ||
|
||
impl Completer for ReplHelper { | ||
type Candidate = CompletionCandidate; | ||
|
||
fn complete( | ||
&self, | ||
line: &str, | ||
pos: usize, | ||
_ctx: &Context<'_>, | ||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> { | ||
let (idx, word) = extract_word(line, pos, None, &[]); | ||
if !line.starts_with(PREFIX) { | ||
return Ok((0, vec![])); | ||
} | ||
let word = word.trim_matches(PREFIX); | ||
|
||
let commands = self | ||
.commands | ||
.iter() | ||
.filter(|cmd| cmd.starts_with(word)) | ||
.map(|x| CompletionCandidate { display: x }) | ||
.collect::<Vec<_>>(); | ||
|
||
Ok((idx + 1, commands)) | ||
} | ||
|
||
// TODO: Complete method names, types, etc. | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
//! The repl implementation for saltwater. | ||
#![deny(rust_2018_idioms)] | ||
|
||
mod commands; | ||
mod helper; | ||
mod repl; | ||
|
||
fn main() { | ||
let mut repl = repl::Repl::new(); | ||
match repl.run() { | ||
Ok(_) => {} | ||
Err(err) => { | ||
println!("error: {}", err); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, these errors are only sometimes showing up.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These lines only print the error that is returned by Imo it's better to remove the "only expressions are valid input" check because it can be really confusing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, but this isn't reporting the error at all, which is a bug. Do you know how to fix that? I'm not sure entirely where the syntax errors are bubbling up - maybe in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The errors are reported in |
||
std::process::exit(1); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks copy/pasted from https://github.com/jyn514/saltwater/pull/515/files#diff-376fa271c6951618907ff944ff30e93aL368. Rather than duplicating the code, can you change that to call this function instead? That way they're consistent.