diff --git a/src/key_command/constants.rs b/src/key_command/constants.rs index a7d4d9a6b..0888e9d86 100644 --- a/src/key_command/constants.rs +++ b/src/key_command/constants.rs @@ -1,4 +1,4 @@ -use rustyline::completion::Pair; +use lazy_static::lazy_static; pub const CMD_COMMAND_LINE: &str = ":"; @@ -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,)*]; } }; } @@ -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 { - commands() - .into_iter() - .filter(|command| command.starts_with(partial_command)) - .map(|command| Pair { - display: command.to_string(), - replacement: command.to_string(), - }) - .collect() -} diff --git a/src/key_command/impl_completion.rs b/src/key_command/impl_completion.rs new file mode 100644 index 000000000..8a5dfc958 --- /dev/null +++ b/src/key_command/impl_completion.rs @@ -0,0 +1,72 @@ +use super::constants::*; +use super::{Command, Completion}; + +pub enum CompletionKind<'a> { + Bin, + Custom(Vec<&'a str>), + Dir(Option>), + File, +} + +impl Completion for Command { + fn completion_kind(cmd: &str) -> Option { + 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, + }) + } +} diff --git a/src/key_command/mod.rs b/src/key_command/mod.rs index f337c0d46..cd50f2ff7 100644 --- a/src/key_command/mod.rs +++ b/src/key_command/mod.rs @@ -6,6 +6,7 @@ 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; @@ -13,5 +14,4 @@ mod impl_numbered; pub use self::command::*; pub use self::command_keybind::*; -pub use self::constants::*; pub use self::traits::*; diff --git a/src/key_command/traits.rs b/src/key_command/traits.rs index 32a344370..ea27db42c 100644 --- a/src/key_command/traits.rs +++ b/src/key_command/traits.rs @@ -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, @@ -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; +} diff --git a/src/ui/views/tui_textfield.rs b/src/ui/views/tui_textfield.rs index f3e3588d1..f28bd13a3 100644 --- a/src/ui/views/tui_textfield.rs +++ b/src/ui/views/tui_textfield.rs @@ -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}; @@ -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 {} @@ -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 } @@ -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 } @@ -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 { + 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)> { - 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 = 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)) + } + } } } };