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 { pub fn new() -> Self { CommandBox { history: History::new(64), hist_idx: None, text: String::new(), cursor: 0, completions: None, } } pub fn invalidate_completions(&mut self) { if self.completions.is_some() { self.completions = None; } } pub fn forward(&mut self) { 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) { let mut prev_word_idx = 0; { let sl = &self.text[0..self.cursor]; let idx = sl.rfind(|c: char| !c.is_alphanumeric() && c != '_'); if let Some(i) = idx { prev_word_idx = i; } } self.cursor = prev_word_idx; self.invalidate_completions(); } pub fn cursor_forward_word(&mut self) { let mut next_word_idx = self.cursor; { if self.cursor != self.text.len() { let sl = &self.text[self.cursor..]; let idx = sl.find(|c: char| !c.is_alphanumeric() && c != '_'); if let Some(i) = idx { next_word_idx = i; } } } 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) { if self.cursor != 0 { 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 { self.text.is_empty() } 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 { let size = self.history.items.len(); self.history.items[size - 1 - self.hist_idx.unwrap()].clone() } pub fn hist_prev(&mut self) { if self.history.items.is_empty() { return; } if let Some(idx) = self.hist_idx { if idx + 1 < self.history.items.len() { self.hist_idx = Some(idx + 1); self.text = self.get_from_hist(); self.cursor_end(); } } else { self.hist_idx = Some(0); self.text = self.get_from_hist(); self.cursor_end(); } self.invalidate_completions(); } pub fn hist_next(&mut self) { if let Some(idx) = self.hist_idx { // most recent hist item, reset command box if idx == 0 { self.hist_idx = None; self.text = "(".into(); } else { self.hist_idx = Some(idx - 1); self.text = self.get_from_hist(); } self.cursor_end(); } self.invalidate_completions(); } pub fn complete(&mut self, env_list: &[Environment], reverse: bool) { 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 = if reverse { cs.prev() } else { 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(env_list, reverse) } } } impl std::default::Default for CommandBox { fn default() -> Self { CommandBox::new() } } #[derive(Debug)] pub struct History { pub items: Vec, pub max_size: usize, } impl History { pub fn new(max_size: usize) -> Self { if max_size == 0 { panic!(); } Self { items: vec![], max_size, } } pub fn append(&mut self, item: T) { if self.items.len() >= self.max_size { self.items.remove(0); } self.items.push(item); } } #[cfg(test)] mod command_tests { use super::*; fn setup_with(text: &str) -> CommandBox { let mut cmd = CommandBox::new(); cmd.push_str(text); cmd } #[test] fn entering_text() { let cmd = setup_with("save as file.png"); assert_eq!(&cmd.text, "save as file.png"); assert_eq!(cmd.cursor, 16) } #[test] fn entering_text_between() { let mut cmd = setup_with("save as file.png"); cmd.backward(); cmd.backward(); cmd.backward(); cmd.push_str("ext"); assert_eq!(&cmd.text, "save as file.extpng"); } #[test] fn delete_to_end() { let mut cmd = setup_with("save as file.png"); cmd.backward(); cmd.backward(); cmd.backward(); cmd.delete_to_end(); assert_eq!(&cmd.text, "save as file."); } #[test] fn delete_to_start() { let mut cmd = setup_with("save as file.png"); cmd.backward(); cmd.backward(); cmd.backward(); cmd.delete_to_start(); assert_eq!(&cmd.text, "png"); } #[test] fn backspacing_from_end() { let mut cmd = setup_with("save"); cmd.backspace(); assert_eq!(&cmd.text, "sav"); assert_eq!(cmd.cursor, 3); } #[test] fn backspacing_from_middle() { let mut cmd = setup_with("save"); cmd.backward(); cmd.backspace(); assert_eq!(&cmd.text, "sae"); assert_eq!(cmd.cursor, 2); } #[test] fn delete() { let mut cmd = setup_with("save"); cmd.backward(); cmd.delete(); assert_eq!(&cmd.text, "sav"); assert_eq!(cmd.cursor, 3); } #[test] fn delete_end() { let mut cmd = setup_with("save"); cmd.delete(); assert_eq!(&cmd.text, "save"); } #[test] fn delete_all() { let mut cmd = setup_with("save"); for _ in 0..4 { cmd.backward(); } for _ in 0..4 { cmd.delete(); } assert_eq!(&cmd.text, ""); assert_eq!(cmd.cursor, 0); } #[test] fn seeking() { let mut cmd = setup_with("save"); for _ in 0..4 { cmd.backward(); } assert_eq!(cmd.cursor, 0); cmd.forward(); 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"); cmd.hist_append(); cmd.push_str("another"); cmd.hist_append(); cmd.push_str("one"); cmd.hist_append(); assert_eq!(cmd.history.items.len(), 3); } #[test] fn hist_prev() { let mut cmd = setup_with("hello"); cmd.hist_append(); cmd.push_str("another"); cmd.hist_append(); cmd.push_str("one"); cmd.hist_append(); cmd.hist_prev(); assert_eq!(&cmd.text, "one"); cmd.hist_prev(); assert_eq!(&cmd.text, "another"); } #[test] fn hist_next() { let mut cmd = setup_with("hello"); cmd.hist_append(); cmd.push_str("another"); cmd.hist_append(); cmd.push_str("one"); cmd.hist_append(); cmd.hist_prev(); cmd.hist_prev(); cmd.hist_next(); assert_eq!(&cmd.text, "one"); } } #[cfg(test)] mod history_tests { use super::*; #[test] fn append() { let mut h = History::::new(4); h.append(5); h.append(6); h.append(7); h.append(8); h.append(9); assert_eq!(h.items, vec![6, 7, 8, 9]); } }