Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5b35f03
initial
George-lewis Jun 14, 2021
062e736
a little cleaner
George-lewis Jun 14, 2021
8446075
lint
George-lewis Jun 14, 2021
a0992c7
refactor
George-lewis Jun 14, 2021
1fa5725
lint
George-lewis Jun 14, 2021
b554d85
delete file
George-lewis Jun 14, 2021
fafaa59
cargo fix
George-lewis Jun 14, 2021
667c6b6
fmt
George-lewis Jun 14, 2021
a099f8d
improve hinting and highlighting
George-lewis Jun 14, 2021
a535a45
lint
George-lewis Jun 14, 2021
0dab155
refactor completion logic
George-lewis Jun 14, 2021
5616135
lint
George-lewis Jun 14, 2021
8b43d53
move comment
George-lewis Jun 14, 2021
f29d383
refactor
George-lewis Jun 14, 2021
411e906
fix
George-lewis Jun 14, 2021
066e8d6
lint
George-lewis Jun 14, 2021
6028f1a
remove file
George-lewis Jun 14, 2021
7b1a83c
rename
George-lewis Jun 14, 2021
75f6209
partially centralize prefixes, fix completion bounds
George-lewis Jun 14, 2021
e3b7744
centralize prefixes
George-lewis Jun 14, 2021
0d4962d
lint
George-lewis Jun 14, 2021
d64d5a8
docs
George-lewis Jun 14, 2021
e1e267a
remove custom types where possible
George-lewis Jun 14, 2021
a880ea2
lint
George-lewis Jun 14, 2021
bc344a9
fix
George-lewis Jun 14, 2021
cf28be7
fix and simplify completion / hint logic
George-lewis Jun 14, 2021
3a0d0a6
docs
George-lewis Jun 14, 2021
27b7ddd
simplify
George-lewis Jun 14, 2021
08ead55
remove deadcode
George-lewis Jun 14, 2021
7e353e7
remove deadcode
George-lewis Jun 14, 2021
6698d81
lint
George-lewis Jun 14, 2021
44e2101
don't have a cow, man!
George-lewis Jun 14, 2021
0c8a3ee
lint
George-lewis Jun 14, 2021
31a02d4
Merge branch 'master' of https://github.com/George-lewis/rustcalc int…
George-lewis Jun 14, 2021
99400a0
version bump: 1.5.0
George-lewis Jun 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustcalc"
version = "1.4.1"
version = "1.5.0"
authors = ["George-Lewis <georgejeffreylewis@gmail.com>"]
edition = "2018"
description = "A command-line utility for evaluating mathematical statements"
Expand Down
49 changes: 49 additions & 0 deletions src/cli/editor/completer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use rustyline::completion::{Completer, Pair};

use super::{
finder::{find_items, Findable},
MyHelper,
};

fn find_candidates<Item: Findable>(line: &str, items: &[Item]) -> Option<Vec<Pair>> {
let create_intermediate = |stride, item: &Item| {
let replacement = item.replacement()[stride..].to_string();
let display = item.format();
Pair {
display,
replacement,
}
};
find_items(line, items, create_intermediate)
}

impl Completer for MyHelper<'_> {
type Candidate = Pair;

fn complete(
&self,
line: &str,
pos: usize,
_ctx: &rustyline::Context<'_>,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
let line = &line[..pos];

let funcs = self.funcs.borrow();
let vars = self.vars.borrow();

let candidates = if let Some(candidates) = find_candidates(line, &funcs) {
(pos, candidates)
} else if let Some(candidates) = find_candidates(line, &vars) {
(pos, candidates)
} else {
(0, vec![])
};

rustyline::Result::Ok(candidates)
}

fn update(&self, line: &mut rustyline::line_buffer::LineBuffer, start: usize, elected: &str) {
let end = line.pos();
line.replace(start..end, elected);
}
}
128 changes: 128 additions & 0 deletions src/cli/editor/finder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use std::borrow::Cow;

use rustmatheval::model::{
functions::{Function, PREFIX as FUNCTION_PREFIX},
variables::{Variable, PREFIX as VARIABLE_PREFIX},
};

use crate::funcs::format_func_with_args;
use crate::vars::format_var_name;

/// Find the position of the last instance of `c`
///
/// ## Examples
///
/// ```
/// use rustmatheval::model::functions::PREFIX;
/// let s = format1("abc {}foo bar", PREFIX);
/// let pos = find_last(PREFIX, s).unwrap();
/// assert_eq!(s.chars().nth(pos), PREFIX);
/// ```
pub fn find_last(c: char, str: &str) -> Option<usize> {
str.chars()
.into_iter()
.rev()
.position(|ch| ch == c)
.map(|pos| str.chars().count() - pos - 1)
}

/// Find all possibly completable `Item`s in `line` at the position closest to the end as indicated by [`Findable::prefix`]
/// and perform transformations on it using the two `Fn` parameters.
///
/// ## Parameters
/// * `Item` - A [`Findable`] type to search for in `line`
/// * `Intermediate` - The output type
/// * `ToIntermediate` - A `Fn` type capable of converting `Item`s to `Intermediate`
///
/// ## Arguments
/// * `line` - The string to find items within
/// * `items` - A slice of items, these are candidates for the search
/// * `create_intermediate` - A `Fn` that:
/// * accepts:
/// * `stride: usize`: The stride of the matching section
/// * `item: &Item`: The matching item
/// * and produces an `Intermediate`
///
/// ## Returns
/// Returns `None` if there are no possible matches inside the input stringThere are two reasons this could occur:
/// * There is no prefix in the string, and thus no matches are possible
/// * There is a prefix in the string, but none of the items could possibly complete the identifier
///
/// Otherwise: Returns a vector of intermediates
pub(super) fn find_items<Item, Intermediate, ToIntermediate>(
line: &str,
items: &[Item],
create_intermediate: ToIntermediate,
) -> Option<Vec<Intermediate>>
where
Item: Findable,
ToIntermediate: Fn(usize, &Item) -> Intermediate,
{
if let Some(pos) = find_last(Item::prefix(), line) {
// +1 removes prefix
// e.g. "#foobar" => "foobar"
let line = &line[pos + 1..];
let stride = line.len();

let matches: Vec<Intermediate> = items
.iter()
.filter(|it| it.name().starts_with(line))
.map(|it| create_intermediate(stride, it))
.collect();
if !matches.is_empty() {
return Some(matches);
}
}
None
}

/// Represents a type that can be found (using [`find_items`])
pub trait Findable {
fn name(&self) -> &str;
fn replacement(&self) -> Cow<'_, str>;
fn format(&self) -> String;
fn prefix() -> char;
}

impl Findable for Function {
fn name(&self) -> &str {
&self.name
}

fn replacement(&self) -> Cow<'_, str> {
let appendix = if self.arity() == 0 {
// If the function takes no arguments we can just open and close the parens
"()"
} else {
"("
};
let formatted = format!("{}{}", &self.name, appendix);
Cow::Owned(formatted)
}

fn format(&self) -> String {
format_func_with_args(self)
}

fn prefix() -> char {
FUNCTION_PREFIX
}
}

impl Findable for Variable {
fn name(&self) -> &str {
&self.repr
}

fn replacement(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.repr)
}

fn format(&self) -> String {
format_var_name(&self.repr).to_string()
}

fn prefix() -> char {
VARIABLE_PREFIX
}
}
37 changes: 37 additions & 0 deletions src/cli/editor/highlighter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::borrow::Cow;

use colored::Colorize;
use rustyline::highlight::Highlighter;

use super::MyHelper;

impl Highlighter for MyHelper<'_> {
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> std::borrow::Cow<'l, str> {
Cow::Borrowed(line)
}

fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> std::borrow::Cow<'b, str> {
Cow::Borrowed(prompt)
}

fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
Cow::Owned(hint.black().on_white().to_string())
}

fn highlight_candidate<'c>(
&self,
candidate: &'c str,
_completion: rustyline::CompletionType,
) -> std::borrow::Cow<'c, str> {
// We don't highlight the candidate because the completer formats with color
Cow::Borrowed(candidate)
}

fn highlight_char(&self, _line: &str, _pos: usize) -> bool {
true
}
}
25 changes: 25 additions & 0 deletions src/cli/editor/hinter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use rustyline::hint::Hinter;

use super::{
finder::{find_items, Findable},
MyHelper,
};

pub fn find_hint<Item: Findable>(line: &str, items: &[Item]) -> Option<String> {
let create_intermediate = |stride, item: &Item| item.replacement()[stride..].to_string();
let hints = find_items(line, items, create_intermediate);
hints.and_then(|hints| hints.into_iter().max_by_key(String::len))
}

impl Hinter for MyHelper<'_> {
type Hint = String;

fn hint(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
let line = &line[..pos];

let funcs = self.funcs.borrow();
let vars = self.vars.borrow();

find_hint(line, &funcs).or_else(|| find_hint(line, &vars))
}
}
32 changes: 32 additions & 0 deletions src/cli/editor/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use std::cell::RefCell;

use rustmatheval::model::{functions::Function, variables::Variable};
use rustyline::{config::Configurer, Editor, Helper};

mod completer;
mod finder;
mod highlighter;
mod hinter;
mod validator;

pub fn editor<'a>(
funcs: &'a RefCell<Vec<Function>>,
vars: &'a RefCell<Vec<Variable>>,
) -> Editor<MyHelper<'a>> {
let mut editor = Editor::<MyHelper>::new();

let helper = MyHelper { funcs, vars };

editor.set_helper(Some(helper));

editor.set_completion_type(rustyline::CompletionType::List);

editor
}

pub struct MyHelper<'cell> {
pub funcs: &'cell RefCell<Vec<Function>>,
pub vars: &'cell RefCell<Vec<Variable>>,
}

impl Helper for MyHelper<'_> {}
16 changes: 16 additions & 0 deletions src/cli/editor/validator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use rustyline::validate::{ValidationResult, Validator};

use super::MyHelper;

impl Validator for MyHelper<'_> {
fn validate(
&self,
_ctx: &mut rustyline::validate::ValidationContext,
) -> rustyline::Result<ValidationResult> {
Ok(rustyline::validate::ValidationResult::Valid(None))
}

fn validate_while_typing(&self) -> bool {
false
}
}
13 changes: 10 additions & 3 deletions src/cli/funcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,18 @@ pub fn format_func_name(name: &str) -> ColoredString {
format!("#{}", name.magenta().bold()).normal()
}

fn format_func(func: &Function, funcs: &[Function], vars: &[Variable]) -> String {
pub fn format_func_with_args(func: &Function) -> String {
format!(
"[ {}({}) = {} ]",
"{}({})",
format_func_name(&func.name),
func.args.iter().map(color_arg).join(", "),
func.args.iter().map(color_arg).join(", ")
)
}

fn format_func(func: &Function, funcs: &[Function], vars: &[Variable]) -> String {
format!(
"[ {} = {} ]",
format_func_with_args(func),
stringify_func_code(func, funcs, vars)
)
}
Expand Down
16 changes: 8 additions & 8 deletions src/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod cli;
mod config;
mod editor;
mod error;
mod funcs;
mod rcfile;
Expand All @@ -13,14 +14,13 @@ use lib::{doeval, model::EvaluationContext};
pub use rustmatheval as lib;

use config::HISTORY_FILE;
use rustyline::Editor;

use error::Error;
use std::{env, process};
use std::{cell::RefCell, env, process};

use cli::{handle_errors, handle_input};

use crate::cli::handle_library_errors;
use crate::{cli::handle_library_errors, editor::editor};

pub fn main() -> ! {
// One-shot mode
Expand Down Expand Up @@ -51,10 +51,10 @@ pub fn main() -> ! {
process::exit(code);
}

let mut vars = vec![];
let mut funcs = vec![];
let vars = RefCell::new(vec![]);
let funcs = RefCell::new(vec![]);

if let Err(inner) = rcfile::load(&mut vars, &mut funcs) {
if let Err(inner) = rcfile::load(&mut vars.borrow_mut(), &mut funcs.borrow_mut()) {
match inner {
Error::Io(inner) => {
println!("Error loading RCFile: {:#?}", inner);
Expand All @@ -63,7 +63,7 @@ pub fn main() -> ! {
}
};

let mut editor = Editor::<()>::new();
let mut editor = editor(&funcs, &vars);

if let Some(path) = HISTORY_FILE.as_deref() {
editor.load_history(path).ok();
Expand All @@ -88,7 +88,7 @@ pub fn main() -> ! {
// Add the line to the history
editor.add_history_entry(&input);

match handle_input(&input, &mut vars, &mut funcs) {
match handle_input(&input, &mut vars.borrow_mut(), &mut funcs.borrow_mut()) {
Ok(formatted) => println!("{}", formatted),
Err(error) => {
let msg = handle_errors(&error, &input);
Expand Down
Loading