tab complete! yay!
Signed-off-by: Ava Hahn <ava@sunnypup.io>
This commit is contained in:
parent
c2cc8571bc
commit
1ef0a534db
2 changed files with 216 additions and 19 deletions
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue