Skip to content

Commit

Permalink
some completion improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
mitnk committed Nov 18, 2022
1 parent a20d1ee commit f7f5443
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 88 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# cicada Release Notes

## 0.9.33 - master
## 0.9.33 - 2022.11.18

- added multiple-line mode.
- some completion improvements.

## 0.9.32 - 2022.07.17

Expand Down
3 changes: 3 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ $ make install

cicada will be installed under `/usr/local/bin`

> I found on newer MacOS, a reboot is needed after generating a new binary.
> This may be an bug/feature of the OS security things.
## Set cicada as your login shell

**WARNING**: Please test cicada on your system before setting it as default
Expand Down
71 changes: 30 additions & 41 deletions src/completers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod env;
pub mod make;
pub mod path;
pub mod ssh;
pub mod utils;

use crate::libs;
use crate::parsers;
Expand Down Expand Up @@ -37,10 +38,9 @@ fn for_cd(line: &str) -> bool {
}

fn for_bin(line: &str) -> bool {
// TODO: why 'echo hi|ech<TAB>' doesn't complete in real?
// but passes in test cases?
let ptn = r"(^ *(sudo|which)? *[a-zA-Z0-9_\.-]+$)|(^.+\| *(sudo|which)? *[a-zA-Z0-9_\.-]+$)";
libs::re::re_contains(line, ptn)
let ptn = r"(^ *(sudo|which|nohup)? *[a-zA-Z0-9_\.-]+$)|(^.+\| *(sudo|which|nohup)? *[a-zA-Z0-9_\.-]+$)";
let result = libs::re::re_contains(line, ptn);
return result;
}

fn for_dots(line: &str) -> bool {
Expand All @@ -64,53 +64,42 @@ impl<Term: Terminal> Completer<Term> for CicadaCompleter {
) -> Option<Vec<Completion>> {
let line = reader.buffer();

// these completions should not fail back to path completion.
if for_bin(line) {
let completions: Option<Vec<Completion>>;
if for_dots(line) {
let cpl = Arc::new(dots::DotsCompleter);
completions = cpl.complete(word, reader, start, _end);
} else if for_ssh(line) {
let cpl = Arc::new(ssh::SshCompleter);
completions = cpl.complete(word, reader, start, _end);
} else if for_make(line) {
let cpl = Arc::new(make::MakeCompleter);
completions = cpl.complete(word, reader, start, _end);
} else if for_bin(line) {
let cpl = Arc::new(path::BinCompleter {
sh: self.sh.clone(),
});
return cpl.complete(word, reader, start, _end);
}
if for_cd(line) {
completions = cpl.complete(word, reader, start, _end);
} else if for_env(line) {
let cpl = Arc::new(env::EnvCompleter);
completions = cpl.complete(word, reader, start, _end);
} else if for_cd(line) {
// `for_cd` should be put a bottom position, so that
// `cd $SOME_ENV_<TAB>` works as expected.
let cpl = Arc::new(path::CdCompleter);
// completions for `cd` should not fail back to path-completion
return cpl.complete(word, reader, start, _end);
} else {
completions = None;
}

// the following completions needs fail back to use path completion,
// so that `$ make generate /path/to/fi<Tab>` still works.
if for_ssh(line) {
let cpl = Arc::new(ssh::SshCompleter);
if let Some(x) = cpl.complete(word, reader, start, _end) {
if !x.is_empty() {
return Some(x);
}
}
}
if for_make(line) {
let cpl = Arc::new(make::MakeCompleter);
if let Some(x) = cpl.complete(word, reader, start, _end) {
if !x.is_empty() {
return Some(x);
}
}
}
if for_env(line) {
let cpl = Arc::new(env::EnvCompleter);
if let Some(x) = cpl.complete(word, reader, start, _end) {
if !x.is_empty() {
return Some(x);
}
}
}
if for_dots(line) {
let cpl = Arc::new(dots::DotsCompleter);
if let Some(x) = cpl.complete(word, reader, start, _end) {
if !x.is_empty() {
return Some(x);
}
if let Some(x) = completions {
if !x.is_empty() {
return Some(x);
}
}

// empty completions should fail back to path-completion,
// so that `$ make generate /path/to/fi<Tab>` still works.
let cpl = Arc::new(path::PathCompleter);
cpl.complete(word, reader, start, _end)
}
Expand Down
97 changes: 73 additions & 24 deletions src/completers/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ use std::fs::read_dir;
use std::io::Write;
use std::iter::FromIterator;
use std::os::unix::fs::PermissionsExt;
use std::path::{is_separator, MAIN_SEPARATOR};
use std::path::MAIN_SEPARATOR;
use std::sync::Arc;

use linefeed::complete::{Completer, Completion, Suffix};
use linefeed::terminal::Terminal;
use linefeed::Prompter;

use crate::completers::utils;
use crate::libs;
use crate::parsers;
use crate::shell;
Expand All @@ -22,6 +23,17 @@ pub struct BinCompleter {
pub struct CdCompleter;
pub struct PathCompleter;

fn is_env_prefix(line: &str) -> bool {
libs::re::re_contains(line, r" *\$[a-zA-Z_][A-Za-z0-9_]*")
}

fn is_pipelined(path: &str) -> bool {
if !path.contains('|') {
return false;
}
!path.starts_with('"') && !path.starts_with('\'')
}

impl<Term: Terminal> Completer<Term> for BinCompleter {
fn complete(
&self,
Expand Down Expand Up @@ -69,6 +81,7 @@ fn needs_expand_home(line: &str) -> bool {

/// Returns a sorted list of paths whose prefix matches the given path.
pub fn complete_path(word: &str, for_dir: bool) -> Vec<Completion> {
let is_env = is_env_prefix(word);
let mut res = Vec::new();
let linfo = parsers::parser_line::parse_line(word);
let tokens = linfo.tokens;
Expand All @@ -79,14 +92,25 @@ pub fn complete_path(word: &str, for_dir: bool) -> Vec<Completion> {
(_path.clone(), _path_sep.clone())
};

let (_dir_orig, _) = split_path(&path);
let dir_orig = if let Some(_dir) = _dir_orig { _dir } else { "" };
let (_, _dir_orig, _f) = split_pathname(&path, "");
let dir_orig = if _dir_orig.is_empty() {
String::new()
} else {
_dir_orig.clone()
};
let mut path_extended = path.clone();
if needs_expand_home(&path_extended) {
shell::expand_home_string(&mut path_extended)
utils::expand_home_string(&mut path_extended)
}
let (_dir_lookup, file_name) = split_path(&path_extended);
let dir_lookup = _dir_lookup.unwrap_or(".");
utils::expand_env_string(&mut path_extended);

let (_, _dir_lookup, file_name) = split_pathname(&path_extended, "");
let dir_lookup = if _dir_lookup.is_empty() {
".".to_string()
} else {
_dir_lookup.clone()
};
// let dir_lookup = _dir_lookup.unwrap_or(".");
if let Ok(entries) = read_dir(dir_lookup) {
for entry in entries {
if let Ok(entry) = entry {
Expand All @@ -99,7 +123,7 @@ pub fn complete_path(word: &str, for_dir: bool) -> Vec<Completion> {
let entry_name = entry.file_name();
// TODO: Deal with non-UTF8 paths in some way
if let Ok(_path) = entry_name.into_string() {
if _path.starts_with(file_name) {
if _path.starts_with(&file_name) {
let (name, display) = if dir_orig != "" {
(
format!("{}{}{}", dir_orig, MAIN_SEPARATOR, _path),
Expand All @@ -109,7 +133,7 @@ pub fn complete_path(word: &str, for_dir: bool) -> Vec<Completion> {
(_path, None)
};
let mut name = str::replace(name.as_str(), "//", "/");
if path_sep.is_empty() {
if path_sep.is_empty() && !is_env {
name = tools::escape_path(&name);
}
let mut quoted = false;
Expand Down Expand Up @@ -139,18 +163,29 @@ pub fn complete_path(word: &str, for_dir: bool) -> Vec<Completion> {
res
}

fn split_path(path: &str) -> (Option<&str>, &str) {
match path.rfind(is_separator) {
Some(pos) => (Some(&path[..=pos]), &path[pos + 1..]),
None => (None, path),
// Split optional directory and prefix. (see its test cases for more details)
fn split_pathname(path: &str, prefix: &str) -> (String, String, String) {
if is_pipelined(path) {
let tokens: Vec<&str> = path.rsplitn(2, '|').collect();
let prefix = format!("{}|", tokens[1]);
return split_pathname(tokens[0], &prefix);
}
match path.rfind('/') {
Some(pos) => (
prefix.to_string(),
(&path[..=pos]).to_string(),
(&path[pos + 1..]).to_string(),
),
None => (prefix.to_string(), String::new(), path.to_string()),
}
}

/// Returns a sorted list of paths whose prefix matches the given path.
fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {
let mut res = Vec::new();
let (_, fname) = split_path(path);
let (prefix, _, fname) = split_pathname(path, "");
let env_path;

match env::var("PATH") {
Ok(x) => env_path = x,
Err(e) => {
Expand All @@ -163,7 +198,7 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {

// handle alias, builtins, and functions
for func in sh.funcs.keys() {
if !func.starts_with(fname) {
if !func.starts_with(&fname) {
continue;
}
if checker.contains(func) {
Expand All @@ -177,7 +212,7 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {
});
}
for alias in sh.alias.keys() {
if !alias.starts_with(fname) {
if !alias.starts_with(&fname) {
continue;
}
if checker.contains(alias) {
Expand All @@ -192,12 +227,11 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {
}

let builtins = vec![
"alias", "bg", "cd", "cinfo", "exec", "exit", "export", "fg",
"history", "jobs", "read", "source", "ulimit", "unalias", "vox",
"minfd", "set",
"alias", "bg", "cd", "cinfo", "exec", "exit", "export", "fg", "history", "jobs", "read",
"source", "ulimit", "unalias", "vox", "minfd", "set",
];
for item in &builtins {
if !item.starts_with(fname) {
if !item.starts_with(&fname) {
continue;
}
if checker.contains(item.clone()) {
Expand All @@ -219,7 +253,7 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {
for entry in list {
if let Ok(entry) = entry {
if let Ok(name) = entry.file_name().into_string() {
if name.starts_with(fname) {
if name.starts_with(&fname) {
let _mode;
match entry.metadata() {
Ok(x) => _mode = x,
Expand All @@ -242,6 +276,7 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {
checker.insert(name.clone());
// TODO: need to handle quoted: `$ "foo#bar"`
let name_e = tools::escape_path(&name);
let name_e = format!("{}{}", prefix, name_e);
res.push(Completion {
completion: name_e,
display,
Expand All @@ -259,12 +294,26 @@ fn complete_bin(sh: &shell::Shell, path: &str) -> Vec<Completion> {
#[cfg(test)]
mod tests {
use super::needs_expand_home;
use super::split_path;
use super::split_pathname;

#[test]
fn test_split_path() {
assert_eq!(split_path(""), (None, ""));
assert_eq!(split_path(""), (None, ""));
fn test_split_pathname() {
assert_eq!(
split_pathname("", ""),
(String::new(), String::new(), String::new(),)
);
assert_eq!(
split_pathname("hi|ech", ""),
("hi|".to_string(), String::new(), "ech".to_string())
);
assert_eq!(
split_pathname("hi|/bin/ech", ""),
("hi|".to_string(), "/bin/".to_string(), "ech".to_string())
);
assert_eq!(
split_pathname("foo", "aprefix"),
("aprefix".to_string(), String::new(), "foo".to_string())
);
}

#[test]
Expand Down
52 changes: 52 additions & 0 deletions src/completers/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use regex::Regex;
use std::env;

use crate::libs;
use crate::tools;

pub fn expand_home_string(text: &mut String) {
let v = vec![
r"(?P<head> +)~(?P<tail> +)",
r"(?P<head> +)~(?P<tail>/)",
r"^(?P<head> *)~(?P<tail>/)",
r"(?P<head> +)~(?P<tail> *$)",
];
for item in &v {
let re;
if let Ok(x) = Regex::new(item) {
re = x;
} else {
return;
}
let home = tools::get_user_home();
let ss = text.clone();
let to = format!("$head{}$tail", home);
let result = re.replace_all(ss.as_str(), to.as_str());
*text = result.to_string();
}
}

pub fn expand_env_string(text: &mut String) {
// expand "$HOME/.local/share" to "/home/tom/.local/share"
if !text.starts_with('$') {
return;
}
let ptn = r"^\$([A-Za-z_][A-Za-z0-9_]*)";
let mut env_value = String::new();
match libs::re::find_first_group(ptn, &text) {
Some(x) => {
if let Ok(val) = env::var(&x) {
env_value = val;
}
}
None => {
return;
}
}

if env_value.is_empty() {
return;
}
let t = text.clone();
*text = libs::re::replace_all(&t, &ptn, &env_value);
}
Loading

0 comments on commit f7f5443

Please sign in to comment.