From 1ef0a534db8ffd7ed5de957b912d6daf38349608 Mon Sep 17 00:00:00 2001 From: Ava Hahn Date: Wed, 3 May 2023 15:13:18 -0700 Subject: [PATCH] tab complete! yay! Signed-off-by: Ava Hahn --- src/bin/relish.rs | 231 ++++++++++++++++++++++++++++++++++++++++++---- src/sym.rs | 4 + 2 files changed, 216 insertions(+), 19 deletions(-) diff --git a/src/bin/relish.rs b/src/bin/relish.rs index 9f1ea40..e5b7acd 100644 --- a/src/bin/relish.rs +++ b/src/bin/relish.rs @@ -15,25 +15,43 @@ * along with this program. If not, see . */ -use relish::ast::{eval, lex, Ctr, Seg, SymTable, run, load_defaults, load_environment}; -use relish::stdlib::{dynamic_stdlib, static_stdlib}; -use relish::aux::ShellState; - -use std::cell::RefCell; -use std::rc::Rc; -use std::borrow::Cow; -use std::env; - -use nix::unistd; -use nu_ansi_term::{Color, Style}; -use dirs::home_dir; -use reedline::{ - FileBackedHistory, DefaultHinter, DefaultValidator, Reedline, Signal, - Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, +use { + relish::{ + ast::{ + eval, lex, run, + Ctr, Seg, SymTable, + load_defaults, load_environment + }, + stdlib::{ + static_stdlib, dynamic_stdlib + }, + aux::ShellState, + }, + std::{ + cell::RefCell, + rc::Rc, + borrow::Cow, + env, + env::current_dir, + path::{PathBuf, Path}, + }, + reedline::{ + FileBackedHistory, DefaultHinter, DefaultValidator, + Reedline, Signal, Prompt, PromptEditMode, PromptHistorySearch, + PromptHistorySearchStatus, Completer, Suggestion, Span, + KeyModifiers, KeyCode, ReedlineEvent, Keybindings, + ColumnarMenu, Emacs, ReedlineMenu, + default_emacs_keybindings, + }, + nix::unistd, + nu_ansi_term::{Color, Style}, + dirs::home_dir, }; #[derive(Clone)] pub struct CustomPrompt(String, String, String); +#[derive(Clone)] +pub struct CustomCompleter(Vec); impl Prompt for CustomPrompt { fn render_prompt_left(&self) -> Cow { @@ -72,6 +90,127 @@ impl Prompt for CustomPrompt { } } +impl Completer for CustomCompleter { + fn complete(&mut self, line: &str, pos: usize) -> Vec { + let current_dir_path = current_dir() + .expect("current dir bad?"); + let (tok, is_str, start) = get_token_to_complete(line, pos); + let mut sugg = vec![]; + if !is_str { + let mut offcenter_match = vec![]; + for sym in &self.0 { + if sym.starts_with(tok.as_str()) { + sugg.push(Suggestion { + value: sym.clone(), + description: None, + extra: None, + append_whitespace: false, + span: Span { + start, + end: pos + } + }); + } + + if sym.contains(tok.as_str()) { + offcenter_match.push(sym.clone()); + } + } + + for i in offcenter_match { + sugg.push(Suggestion { + value: i.clone(), + description: None, + extra: None, + append_whitespace: false, + span: Span { + start, + end: pos + } + }); + } + } else { + let mut path: PathBuf = Path::new(&tok).to_path_buf(); + if path.is_relative() { + path = current_dir_path.join(path); + } + + if let Ok(npath) = path.canonicalize() { + path = npath; + } + + if path.exists() && path.is_dir() { + if let Ok(entries) = path.read_dir() { + for entry in entries { + if let Ok(e) = entry { + sugg.push(Suggestion { + value: e.path() + .as_os_str() + .to_str() + .expect("bad dir somehow?") + .to_owned(), + description: None, + extra: None, + append_whitespace: false, + span: Span { + start, + end: pos + } + }); + } + } + } + } else { + // check parent to autocomplete path + if let Some(parent) = path.parent() { + if let Ok(entries) = parent.read_dir() { + for entry in entries { + if let Ok(e) = entry { + if e.file_name() + .to_string_lossy() + .starts_with(path.file_name() + // TODO: dont + .expect("bad file somehow?") + .to_string_lossy() + .to_mut() + .as_str() + ) { + sugg.push(Suggestion { + value: e.path() + .as_os_str() + .to_str() + .expect("bad dir somehow?") + .to_owned(), + description: None, + extra: None, + append_whitespace: false, + span: Span { + start, + end: pos + } + }); + } + } + } + } + } + } + } + sugg + } +} + +fn add_menu_keybindings(keybindings: &mut Keybindings) { + keybindings.add_binding( + KeyModifiers::NONE, + KeyCode::Tab, + ReedlineEvent::UntilFound(vec![ + ReedlineEvent::Menu("completion_menu".to_string()), + ReedlineEvent::MenuNext, + ]), + ); +} + fn main() { const HIST_FILE: &str = "/.relish_hist"; const CONFIG_FILE_DEFAULT: &str = "/.relishrc"; @@ -117,12 +256,25 @@ fn main() { dynamic_stdlib(&mut syms, Some(shell_state_bindings)).unwrap_or_else(|err: String| eprintln!("{}", err)); // setup readline - let mut rl = Reedline::create(); + + let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu")); + let mut keybindings = default_emacs_keybindings(); + add_menu_keybindings(&mut keybindings); + let edit_mode = Box::new(Emacs::new(keybindings)); + let mut rl = Reedline::create() + .with_menu(ReedlineMenu::EngineCompleter(completion_menu)) + .with_edit_mode(edit_mode) + .with_quick_completions(true) + .with_partial_completions(true) + .with_ansi_colors(true); + let maybe_hist: Box; if !hist_file_name.is_empty() { - maybe_hist = Box::new(FileBackedHistory::with_file(5000, hist_file_name.into()) - .expect("error reading history!")); - rl = rl.with_history(maybe_hist); + maybe_hist = Box::new( + FileBackedHistory::with_file(5000, hist_file_name.into()) + .expect("error reading history!") + ); + rl = rl.with_history(maybe_hist); } rl = rl.with_hinter(Box::new( DefaultHinter::default() @@ -132,6 +284,9 @@ fn main() { // repl :) loop { let readline_prompt = make_prompt(&mut syms); + let completer = make_completer(&mut syms); + // realloc with each loop because syms can change + rl = rl.with_completer(Box::new(completer)); let user_doc = rl.read_line(&readline_prompt).unwrap(); match user_doc { Signal::Success(line) => { @@ -199,3 +354,41 @@ fn make_prompt(syms: &mut SymTable) -> CustomPrompt { CustomPrompt(l_str, r_str, d_str) } + +fn make_completer(syms: &mut SymTable) -> CustomCompleter { + let mut toks = vec![]; + for i in syms.keys(){ + toks.push(i.clone()); + } + CustomCompleter(toks) +} + +fn get_token_to_complete(line: &str, pos: usize) -> (String, bool, usize) { + let mut res = String::with_capacity(1); + let mut doc = line.chars().rev(); + for _ in pos..line.len() { + doc.next(); + } + let mut is_str = false; + let mut start_idx = pos; + for idx in (0..pos).rev() { + let iter = doc.next() + .expect(format!("AC: no idx {}", idx) + .as_str()); + match iter { + ' ' | '(' | '\n' => { + break + }, + '\'' | '"' => { + is_str = true; + break + }, + _ => { + start_idx = idx; + res.insert(0, iter); + } + } + } + + return (res, is_str, start_idx) +} diff --git a/src/sym.rs b/src/sym.rs index 114b18b..9104d20 100644 --- a/src/sym.rs +++ b/src/sym.rs @@ -96,6 +96,10 @@ impl SymTable { self.0.iter() } + pub fn keys(&self) -> std::collections::hash_map::Keys { + self.0.keys() + } + pub fn update(&mut self, other: &mut SymTable) { /* updates self with all syms in other that match the following cases: * * sym is not in self