From d2cc31ee49d673f343ce5089071ef3628c3cdc97 Mon Sep 17 00:00:00 2001 From: Akshay Date: Mon, 17 May 2021 16:55:09 +0530 Subject: add tab to complete env variables --- src/app.rs | 18 +++----- src/command.rs | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lisp/eval.rs | 28 ++++++++++++ src/lisp/lex.rs | 1 + 4 files changed, 172 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index 04468a6..aba71f8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -27,7 +27,6 @@ use std::{ path::{Path, PathBuf}, }; -use log::info; use obi::{CompressionType, Image}; use sdl2::{ event::Event, @@ -319,10 +318,7 @@ impl<'ctx> AppState<'ctx> { } pub fn eval_expr(&mut self, expr: &LispExpr) { - let mut evaluator = eval::Evaluator { - app: self, - context: Vec::new(), - }; + let mut evaluator = eval::Evaluator::new(self); match evaluator.eval(expr) { Ok(val) => self.message.set_info(format!("{}", val)), Err(eval_err) => self.message.set_error(format!("{}", eval_err)), @@ -745,7 +741,6 @@ impl<'ctx> AppState<'ctx> { self.mode = Mode::Command; } } - info!("key press: {:?}", &event); match self.mode { Mode::Draw => { match event { @@ -1047,6 +1042,7 @@ impl<'ctx> AppState<'ctx> { Keycode::Up => self.command_box.hist_prev(), Keycode::Down => self.command_box.hist_next(), Keycode::Return => self.eval_command(), + Keycode::Tab => self.command_box.complete_next(&self.lisp_env), Keycode::Escape => { self.command_box.clear(); self.message.text.clear(); @@ -1062,11 +1058,11 @@ impl<'ctx> AppState<'ctx> { _ => (), }, // how does one handle alt keys - // _ if keymod == Mod::LALTMOD => match k { - // Keycode::B => self.command_box.cursor_back_word(), - // Keycode::F => self.command_box.cursor_forward_word(), - // _ => (), - // }, + _ if keymod == Mod::LALTMOD => match k { + Keycode::B => self.command_box.cursor_back_word(), + Keycode::F => self.command_box.cursor_forward_word(), + _ => (), + }, _ => (), }, Event::TextInput { text, .. } => { diff --git a/src/command.rs b/src/command.rs index 52ff7c8..26aeb05 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,9 +1,52 @@ +use crate::lisp::{eval::completions, Environment}; + #[derive(Debug)] pub struct CommandBox { pub history: History, pub hist_idx: Option, pub text: String, pub cursor: usize, + pub completions: Option, +} + +#[derive(Debug)] +pub struct Completions { + pub trigger_idx: usize, + pub candidates: Vec, + pub idx: usize, +} + +impl Completions { + pub fn new(trigger_idx: usize, mut candidates: Vec) -> Self { + candidates.insert(0, "".into()); + Self { + trigger_idx, + candidates, + idx: 0, + } + } + pub fn prefix(&self) -> &str { + self.candidates[0].as_str() + } + pub fn next(&mut self) -> &str { + if self.idx + 1 == self.candidates.len() { + self.idx = 0; + } else { + self.idx += 1; + } + self.get() + } + pub fn prev(&mut self) -> &str { + if self.idx == 0 { + self.idx = self.candidates.len() - 1; + } else { + self.idx -= 1; + } + self.get() + } + fn get(&self) -> &str { + &self.candidates[self.idx] + } } impl CommandBox { @@ -13,6 +56,13 @@ impl CommandBox { hist_idx: None, text: String::new(), cursor: 0, + completions: None, + } + } + + pub fn invalidate_completions(&mut self) { + if self.completions.is_some() { + self.completions = None; } } @@ -20,14 +70,17 @@ impl CommandBox { if self.cursor < self.text.len() { self.cursor += 1; } + self.invalidate_completions(); } pub fn cursor_end(&mut self) { self.cursor = self.text.len(); + self.invalidate_completions(); } pub fn cursor_start(&mut self) { self.cursor = 0; + self.invalidate_completions(); } pub fn cursor_back_word(&mut self) { @@ -40,6 +93,7 @@ impl CommandBox { } } self.cursor = prev_word_idx; + self.invalidate_completions(); } pub fn cursor_forward_word(&mut self) { @@ -54,10 +108,26 @@ impl CommandBox { } } self.cursor = next_word_idx; + self.invalidate_completions(); + } + + // returns the word prefix if completable + pub fn completable_word(&mut self) -> Option<&str> { + // text behind the cursor + let sl = &self.text[0..self.cursor]; + // index of last non-completable character + let idx = sl.rfind(|c: char| !c.is_alphanumeric() && !"!$%&*+-./<=>?^_|#".contains(c)); + // i+1 to cursor contains completable word + match idx { + Some(i) if i == self.cursor - 1 => None, + Some(i) => Some(&self.text[i + 1..self.cursor]), + _ => None, + } } pub fn backward(&mut self) { self.cursor = self.cursor.saturating_sub(1); + self.invalidate_completions(); } pub fn backspace(&mut self) { @@ -65,26 +135,38 @@ impl CommandBox { self.text.remove(self.cursor - 1); self.backward(); } + self.invalidate_completions(); } pub fn delete(&mut self) { if self.cursor < self.text.len() { self.text.remove(self.cursor); } + self.invalidate_completions(); + } + + pub fn delete_n(&mut self, n: usize) { + for _ in 0..n { + self.delete() + } + self.invalidate_completions(); } pub fn delete_to_end(&mut self) { self.text.truncate(self.cursor); + self.invalidate_completions(); } pub fn delete_to_start(&mut self) { self.text = self.text.chars().skip(self.cursor).collect(); self.cursor = 0; + self.invalidate_completions(); } pub fn push_str(&mut self, v: &str) { self.text.insert_str(self.cursor, v); self.cursor += v.len(); + self.invalidate_completions(); } pub fn is_empty(&self) -> bool { @@ -94,11 +176,14 @@ impl CommandBox { pub fn clear(&mut self) { self.text.clear(); self.cursor = 0; + self.hist_idx = None; + self.invalidate_completions(); } pub fn hist_append(&mut self) { self.history.append(self.text.drain(..).collect()); self.cursor_start(); + self.invalidate_completions(); } fn get_from_hist(&self) -> String { @@ -121,6 +206,7 @@ impl CommandBox { self.text = self.get_from_hist(); self.cursor_end(); } + self.invalidate_completions(); } pub fn hist_next(&mut self) { @@ -135,6 +221,36 @@ impl CommandBox { } self.cursor_end(); } + self.invalidate_completions(); + } + + pub fn complete_next(&mut self, env_list: &[Environment]) { + let c = self.cursor; + // completions exist, fill with next completion + if let Some(cs) = &mut self.completions { + self.cursor = cs.trigger_idx; + let prev_len = cs.get().len(); + // skips over the first empty completion + let new_insertion = cs.next(); + self.text = format!( + "{}{}{}", + &self.text[..self.cursor], + new_insertion, + &self.text[self.cursor + prev_len..] + ); + self.cursor += new_insertion.len(); + } + // generate candidates, fill second candidate (first candidate is empty candidate) + else if let Some(prefix) = self.completable_word() { + self.completions = Some(Completions::new( + c, + completions(env_list, prefix) + .iter() + .map(|&s| s.to_owned()) + .collect(), + )); + self.complete_next(env_list) + } } } @@ -273,6 +389,26 @@ mod command_tests { assert_eq!(cmd.cursor, 1); } + #[test] + fn get_last_completable() { + assert_eq!(setup_with("(and").completable_word(), Some("and")); + assert_eq!(setup_with("(and (hello").completable_word(), Some("hello")); + } + + #[test] + fn get_middle_completable() { + let mut cmd = setup_with("(and another"); + cmd.cursor = 4; + assert_eq!(cmd.completable_word(), Some("and")); + } + + #[test] + fn no_complete() { + assert_eq!(setup_with("(and ").completable_word(), None); + assert_eq!(setup_with("(and (").completable_word(), None); + assert_eq!(setup_with("(and ( ").completable_word(), None); + } + #[test] fn hist_append() { let mut cmd = setup_with("hello"); diff --git a/src/lisp/eval.rs b/src/lisp/eval.rs index b39730b..784cba0 100644 --- a/src/lisp/eval.rs +++ b/src/lisp/eval.rs @@ -20,6 +20,15 @@ pub struct Evaluator<'ctx, 'global> { pub context: Context, } +impl<'ctx, 'global> Evaluator<'ctx, 'global> { + pub fn new(app: &'global mut AppState<'ctx>) -> Self { + Self { + app, + context: Vec::new(), + } + } +} + impl<'ctx, 'global> Evaluator<'ctx, 'global> where 'ctx: 'global, @@ -337,6 +346,25 @@ pub fn lookup(env_list: &[Environment], key: &str) -> Result(env_list: &'a [Environment], prefix: &str) -> Vec<&'a str> { + fn helper<'h>(env_list: &'h [Environment], prefix: &str, completions: &mut Vec>) { + if !env_list.is_empty() { + let nearest_env = env_list.last().unwrap(); + completions.push( + nearest_env + .keys() + .filter(|k| k.starts_with(prefix)) + .map(|s| &s[prefix.len()..]) + .collect::>(), + ); + helper(&env_list[..env_list.len() - 1], prefix, completions); + } + } + let mut completions = Vec::new(); + helper(env_list, prefix, &mut completions); + completions.into_iter().flatten().collect() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/lisp/lex.rs b/src/lisp/lex.rs index 754a23f..6e59469 100644 --- a/src/lisp/lex.rs +++ b/src/lisp/lex.rs @@ -235,6 +235,7 @@ fn parse_string(input: &str) -> Result<(usize, Token<'_>), ParseErrorKind> { fn is_ident(ch: char) -> bool { match ch { + // "!$%&*+-./<=>?^_|#" '!' | '$' | '%' | '&' | '*' | '+' | '-' | '.' | '/' | '<' | '=' | '>' | '?' | '^' | '_' | '|' | '#' => true, _ if ch.is_alphanumeric() => true, -- cgit v1.2.3