Skip to content

Commit

Permalink
feat: add real autocompletion for each command (#526)
Browse files Browse the repository at this point in the history
  • Loading branch information
Akmadan23 authored May 19, 2024
1 parent 5e582e0 commit d799b49
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 22 deletions.
17 changes: 3 additions & 14 deletions src/key_command/constants.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustyline::completion::Pair;
use lazy_static::lazy_static;

pub const CMD_COMMAND_LINE: &str = ":";

Expand All @@ -8,8 +8,8 @@ macro_rules! cmd_constants {
pub const $cmd_name: &str = $cmd_value;
)*

pub fn commands() -> Vec<&'static str> {
vec![$($cmd_value,)*]
lazy_static! {
pub static ref COMMANDS: Vec<&'static str> = vec![$($cmd_name,)*];
}
};
}
Expand Down Expand Up @@ -98,14 +98,3 @@ cmd_constants![
(CMD_CUSTOM_SEARCH, "custom_search"),
(CMD_CUSTOM_SEARCH_INTERACTIVE, "custom_search_interactive"),
];

pub fn complete_command(partial_command: &str) -> Vec<Pair> {
commands()
.into_iter()
.filter(|command| command.starts_with(partial_command))
.map(|command| Pair {
display: command.to_string(),
replacement: command.to_string(),
})
.collect()
}
72 changes: 72 additions & 0 deletions src/key_command/impl_completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use super::constants::*;
use super::{Command, Completion};

pub enum CompletionKind<'a> {
Bin,
Custom(Vec<&'a str>),
Dir(Option<Vec<&'a str>>),
File,
}

impl Completion for Command {
fn completion_kind(cmd: &str) -> Option<CompletionKind> {
Some(match cmd {
CMD_CHANGE_DIRECTORY => CompletionKind::Dir(None),
CMD_DELETE_FILES => CompletionKind::Custom(vec![
"--background=false",
"--background=true",
"--noconfirm",
"--permanently",
]),
CMD_NEW_TAB => CompletionKind::Dir(Some(vec!["--current", "--cursor", "--last"])),
CMD_OPEN_FILE_WITH
| CMD_SUBPROCESS_CAPTURE
| CMD_SUBPROCESS_INTERACTIVE
| CMD_SUBPROCESS_SPAWN => CompletionKind::Bin,
CMD_QUIT => CompletionKind::Custom(vec![
"--force",
"--output-current-directory",
"--output-file",
"--output-selected-files",
]),
CMD_SEARCH_INCREMENTAL | CMD_SEARCH_STRING => CompletionKind::File,
CMD_SELECT_FZF | CMD_SELECT_GLOB | CMD_SELECT_REGEX | CMD_SELECT_STRING => {
CompletionKind::Custom(vec![
"--all=false",
"--all=true",
"--deselect=false",
"--deselect=true",
"--toggle=false",
"--toggle=true",
])
}
CMD_SET_CASE_SENSITIVITY => CompletionKind::Custom(vec![
"--type=fzf",
"--type=glob",
"--type=regex",
"--type=string",
"auto",
"insensitive",
"sensitive",
]),
CMD_SET_LINEMODE => CompletionKind::Custom(vec![
"all", "group", "mtime", "none", "perm", "size", "user",
]),
CMD_SORT => CompletionKind::Custom(vec![
"--reverse=false",
"--reverse=true",
"ext",
"lexical",
"mtime",
"natural",
"reverse",
"size",
]),
CMD_SWITCH_LINE_NUMBERS => CompletionKind::Custom(vec!["absolute", "none", "relative"]),
CMD_SYMLINK_FILES => {
CompletionKind::Custom(vec!["--relative=false", "--relative=true"])
}
_ => return None,
})
}
}
2 changes: 1 addition & 1 deletion src/key_command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ pub mod traits;
mod impl_appcommand;
mod impl_appexecute;
mod impl_comment;
mod impl_completion;
mod impl_display;
mod impl_from_str;
mod impl_interactive;
mod impl_numbered;

pub use self::command::*;
pub use self::command_keybind::*;
pub use self::constants::*;
pub use self::traits::*;
6 changes: 6 additions & 0 deletions src/key_command/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use crate::context::AppContext;
use crate::error::AppResult;
use crate::ui::AppBackend;

pub use super::impl_completion::CompletionKind;

pub trait AppExecute {
fn execute(
&self,
Expand Down Expand Up @@ -33,3 +35,7 @@ pub trait AppCommand: AppExecute + std::fmt::Display + std::fmt::Debug {
pub trait CommandComment {
fn comment(&self) -> &'static str;
}

pub trait Completion {
fn completion_kind(cmd: &str) -> Option<CompletionKind>;
}
132 changes: 125 additions & 7 deletions src/ui/views/tui_textfield.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::str::FromStr;
use std::{env, fs};

use rustyline::completion::{Candidate, FilenameCompleter, Pair};
use rustyline::history::SearchDirection;
use rustyline::line_buffer::{self, ChangeListener, DeleteListener, Direction, LineBuffer};
use rustyline::{At, Word};

use lazy_static::lazy_static;
use ratatui::layout::Rect;
use ratatui::widgets::Clear;
use termion::event::{Event, Key};
Expand All @@ -13,11 +17,49 @@ use unicode_width::UnicodeWidthStr;
use crate::context::AppContext;
use crate::event::process_event;
use crate::event::AppEvent;
use crate::key_command::{complete_command, Command, InteractiveExecute};
use crate::key_command::constants::COMMANDS;
use crate::key_command::{Command, Completion, CompletionKind, InteractiveExecute};
use crate::ui::views::TuiView;
use crate::ui::widgets::{TuiMenu, TuiMultilineText};
use crate::ui::AppBackend;

lazy_static! {
static ref EXECUTABLES: Vec<&'static str> = {
let mut vec = vec![];

if let Some(path) = env::var_os("PATH") {
for path in env::split_paths(&path) {
let Ok(files) = fs::read_dir(path) else {
continue;
};

for file in files {
let Ok(file) = file else {
continue;
};

let Ok(metadata) = file.metadata() else {
continue;
};

#[cfg(unix)]
if metadata.mode() & 0o100 == 0 {
// if file is not executable
continue;
}

if let Some(basename) = file.path().file_name() {
let boxed = Box::new(basename.to_string_lossy().to_string());
vec.push(Box::leak(boxed).as_str());
}
}
}
}

vec
};
}

// Might need to be implemented in the future
#[derive(Clone, Debug)]
pub struct DummyListener {}
Expand Down Expand Up @@ -205,6 +247,7 @@ impl<'a> TuiTextField<'a> {
.get(curr_history_index, SearchDirection::Forward)
{
line_buffer.insert_str(0, &s.entry, listener);
line_buffer.move_end();
}
true
}
Expand All @@ -224,6 +267,7 @@ impl<'a> TuiTextField<'a> {
.get(curr_history_index, SearchDirection::Reverse)
{
line_buffer.insert_str(0, &s.entry, listener);
line_buffer.move_end();
}
true
}
Expand Down Expand Up @@ -407,20 +451,94 @@ fn move_to_the_end(line_buffer: &mut LineBuffer) {
}
}

fn pair_from_str(s: &str) -> Pair {
Pair {
display: s.to_string(),
replacement: s.to_string(),
}
}

fn start_of_word(line_buffer: &LineBuffer) -> usize {
line_buffer
.as_str()
.split_at(line_buffer.pos())
.0
.rfind(' ')
.map(|x| x + 1)
.unwrap_or(0)
}

fn complete_word(candidates: &[&str], partial: &str) -> Vec<Pair> {
candidates
.iter()
.filter(|entry| entry.starts_with(partial))
.map(|entry| pair_from_str(entry))
.collect()
}

fn get_candidates(
completer: &FilenameCompleter,
line_buffer: &mut LineBuffer,
) -> Option<(usize, Vec<Pair>)> {
let line = line_buffer.as_str().split_once(' ');
let res = match line {
None => Ok((0, complete_command(line_buffer.as_str()))),
let res = match line_buffer.as_str().split_once(' ') {
None => Ok((0, complete_word(&COMMANDS, line_buffer.as_str()))),

Some((command, _files)) => {
Some((command, arg)) => {
// We want to autocomplete a command if we are inside it.
if line_buffer.pos() <= command.len() {
Ok((0, complete_command(command)))
Ok((0, complete_word(&COMMANDS, command)))
} else {
completer.complete_path(line_buffer.as_str(), line_buffer.pos())
match Command::completion_kind(command)? {
CompletionKind::Bin => {
if arg.starts_with("./") || arg.starts_with('/') {
let (start, list) = completer
.complete_path(line_buffer.as_str(), line_buffer.pos())
.ok()?;

#[cfg(unix)]
let list = list
.into_iter()
.filter(|pair| {
// keep entry only if it's executable
if let Ok(file) = PathBuf::from(pair.replacement()).metadata() {
file.mode() & 0o100 != 0
} else {
false
}
})
.collect();

Ok((start, list))
} else {
Ok((start_of_word(line_buffer), complete_word(&EXECUTABLES, arg)))
}
}

CompletionKind::Custom(v) => {
Ok((start_of_word(line_buffer), complete_word(&v, arg)))
}

CompletionKind::File => {
completer.complete_path(line_buffer.as_str(), line_buffer.pos())
}

CompletionKind::Dir(flags) => {
let (start, list) = completer
.complete_path(line_buffer.as_str(), line_buffer.pos())
.ok()?;

let mut new_list: Vec<Pair> = list
.into_iter()
.filter(|pair| PathBuf::from(pair.replacement()).is_dir())
.collect();

if let Some(flags) = flags {
new_list.append(&mut flags.iter().map(|f| pair_from_str(f)).collect())
}

Ok((start, new_list))
}
}
}
}
};
Expand Down

0 comments on commit d799b49

Please sign in to comment.