Early work on tab completion

This commit is contained in:
2025-03-09 21:59:12 -04:00
parent a51cd6cc61
commit 363e4805b7
13 changed files with 260 additions and 43 deletions

86
src/prompt/comp.rs Normal file
View File

@@ -0,0 +1,86 @@
use rustyline::completion::{Candidate, Completer};
use crate::{expand::cmdsub::expand_cmdsub_string, parse::lex::KEYWORDS, prelude::*};
use super::readline::SynHelper;
impl<'a> Completer for SynHelper<'a> {
type Candidate = String;
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
let mut shenv = self.shenv.clone();
let mut comps = vec![];
shenv.new_input(line);
let mut token_stream = Lexer::new(line.to_string(), &mut shenv).lex();
if let Some(comp_token) = token_stream.pop() {
let raw = comp_token.as_raw(&mut shenv);
let is_cmd = if let Some(token) = token_stream.pop() {
match token.rule() {
TkRule::Sep => true,
_ if KEYWORDS.contains(&token.rule()) => true,
_ => false
}
} else {
true
};
if let TkRule::Ident | TkRule::Whitespace = comp_token.rule() {
if is_cmd {
let cmds = shenv.meta().path_cmds();
comps.extend(cmds.iter().map(|cmd| cmd.to_string()));
comps.retain(|cmd| cmd.starts_with(&raw));
if !comps.is_empty() && comps.len() > 1 {
if get_bin_path("fzf", &self.shenv).is_some() {
if let Some(mut selection) = fzf_comp(&comps, &mut shenv) {
while selection.starts_with(&raw) {
selection = selection.strip_prefix(&raw).unwrap().to_string();
}
comps = vec![selection];
}
}
} else if let Some(mut comp) = comps.pop() {
while comp.starts_with(&raw) {
comp = comp.strip_prefix(&raw).unwrap().to_string();
}
comps = vec![comp];
}
return Ok((pos,comps))
} else {
let (start, matches) = self.file_comp.complete(line, pos, ctx)?;
comps.extend(matches.iter().map(|c| c.display().to_string()));
if !comps.is_empty() && comps.len() > 1 {
if get_bin_path("fzf", &self.shenv).is_some() {
if let Some(selection) = fzf_comp(&comps, &mut shenv) {
return Ok((start, vec![selection]))
} else {
return Ok((start, comps))
}
} else {
return Ok((start, comps))
}
} else if let Some(comp) = comps.pop() {
// Slice off the already typed bit
return Ok((start, vec![comp]))
}
}
}
}
Ok((pos,comps))
}
}
pub fn fzf_comp(comps: &[String], shenv: &mut ShEnv) -> Option<String> {
// All of the fzf wrapper libraries suck
// So we gotta do this now
let echo_args = comps.join("\n");
let echo = format!("echo \"{echo_args}\"");
let fzf = "fzf --height=~30% --layout=reverse --border --border-label=completion";
let command = format!("{echo} | {fzf}");
shenv.ctx_mut().set_flag(ExecFlags::NO_EXPAND); // Prevent any pesky shell injections with filenames like '$(rm -rf /)'
let selection = expand_cmdsub_string(&command, shenv).ok()?;
if selection.is_empty() {
None
} else {
Some(selection.trim().to_string())
}
}

View File

@@ -5,6 +5,7 @@ use rustyline::{config::Configurer, history::{DefaultHistory, History}, ColorMod
pub mod readline;
pub mod highlight;
pub mod validate;
pub mod comp;
fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
let hist_path = std::env::var("FERN_HIST").unwrap_or_default();

View File

@@ -1,11 +1,10 @@
use rustyline::{completion::{Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper};
use rustyline::{completion::{Candidate, Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper};
use crate::prelude::*;
pub struct SynHelper<'a> {
file_comp: FilenameCompleter,
pub file_comp: FilenameCompleter,
pub shenv: &'a mut ShEnv,
pub commands: Vec<String>
}
impl<'a> Helper for SynHelper<'a> {}
@@ -15,7 +14,6 @@ impl<'a> SynHelper<'a> {
Self {
file_comp: FilenameCompleter::new(),
shenv,
commands: vec![]
}
}
@@ -35,12 +33,6 @@ impl<'a> SynHelper<'a> {
impl<'a> Completer for SynHelper<'a> {
type Candidate = String;
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
Ok((0,vec![]))
}
}
pub struct SynHint {
text: String,