tab complete! yay!

Signed-off-by: Ava Hahn <ava@sunnypup.io>
This commit is contained in:
Ava Apples Affine 2023-05-03 15:13:18 -07:00
parent c2cc8571bc
commit 1ef0a534db
Signed by: affine
GPG key ID: 3A4645B8CF806069
2 changed files with 216 additions and 19 deletions

View file

@ -15,25 +15,43 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
use relish::ast::{eval, lex, Ctr, Seg, SymTable, run, load_defaults, load_environment}; use {
use relish::stdlib::{dynamic_stdlib, static_stdlib}; relish::{
use relish::aux::ShellState; ast::{
eval, lex, run,
use std::cell::RefCell; Ctr, Seg, SymTable,
use std::rc::Rc; load_defaults, load_environment
use std::borrow::Cow; },
use std::env; stdlib::{
static_stdlib, dynamic_stdlib
use nix::unistd; },
use nu_ansi_term::{Color, Style}; aux::ShellState,
use dirs::home_dir; },
use reedline::{ std::{
FileBackedHistory, DefaultHinter, DefaultValidator, Reedline, Signal, cell::RefCell,
Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, 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)] #[derive(Clone)]
pub struct CustomPrompt(String, String, String); pub struct CustomPrompt(String, String, String);
#[derive(Clone)]
pub struct CustomCompleter(Vec<String>);
impl Prompt for CustomPrompt { impl Prompt for CustomPrompt {
fn render_prompt_left(&self) -> Cow<str> { fn render_prompt_left(&self) -> Cow<str> {
@ -72,6 +90,127 @@ impl Prompt for CustomPrompt {
} }
} }
impl Completer for CustomCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
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() { fn main() {
const HIST_FILE: &str = "/.relish_hist"; const HIST_FILE: &str = "/.relish_hist";
const CONFIG_FILE_DEFAULT: &str = "/.relishrc"; const CONFIG_FILE_DEFAULT: &str = "/.relishrc";
@ -117,11 +256,24 @@ fn main() {
dynamic_stdlib(&mut syms, Some(shell_state_bindings)).unwrap_or_else(|err: String| eprintln!("{}", err)); dynamic_stdlib(&mut syms, Some(shell_state_bindings)).unwrap_or_else(|err: String| eprintln!("{}", err));
// setup readline // 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<FileBackedHistory>; let maybe_hist: Box<FileBackedHistory>;
if !hist_file_name.is_empty() { if !hist_file_name.is_empty() {
maybe_hist = Box::new(FileBackedHistory::with_file(5000, hist_file_name.into()) maybe_hist = Box::new(
.expect("error reading history!")); FileBackedHistory::with_file(5000, hist_file_name.into())
.expect("error reading history!")
);
rl = rl.with_history(maybe_hist); rl = rl.with_history(maybe_hist);
} }
rl = rl.with_hinter(Box::new( rl = rl.with_hinter(Box::new(
@ -132,6 +284,9 @@ fn main() {
// repl :) // repl :)
loop { loop {
let readline_prompt = make_prompt(&mut syms); 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(); let user_doc = rl.read_line(&readline_prompt).unwrap();
match user_doc { match user_doc {
Signal::Success(line) => { Signal::Success(line) => {
@ -199,3 +354,41 @@ fn make_prompt(syms: &mut SymTable) -> CustomPrompt {
CustomPrompt(l_str, r_str, d_str) 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)
}

View file

@ -96,6 +96,10 @@ impl SymTable {
self.0.iter() self.0.iter()
} }
pub fn keys(&self) -> std::collections::hash_map::Keys<String, Symbol> {
self.0.keys()
}
pub fn update(&mut self, other: &mut SymTable) { pub fn update(&mut self, other: &mut SymTable) {
/* updates self with all syms in other that match the following cases: /* updates self with all syms in other that match the following cases:
* * sym is not in self * * sym is not in self