diff options
author | qkzk <qkzk@users.noreply.github.com> | 2023-12-12 21:41:39 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-12 21:41:39 +0100 |
commit | 1df94a5fe4fb781b6d207bc35759348b3d8ad86a (patch) | |
tree | eec2b9461c74d8ae0e3f2cf8a210e00d7222e284 /src/modes/edit/completion.rs | |
parent | 527204946897d403cdcffc21914ff490dbd185fe (diff) | |
parent | 3ded042f88d2f4708b5ff3cd76df1bf43d31b1e8 (diff) |
Merge pull request #86 from qkzk/v0.1.24-use_rcv0.1.24
V0.1.24
Diffstat (limited to 'src/modes/edit/completion.rs')
-rw-r--r-- | src/modes/edit/completion.rs | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/src/modes/edit/completion.rs b/src/modes/edit/completion.rs new file mode 100644 index 0000000..8d6f739 --- /dev/null +++ b/src/modes/edit/completion.rs @@ -0,0 +1,278 @@ +use std::fmt; +use std::fs::{self, ReadDir}; + +use anyhow::Result; +use strum::IntoEnumIterator; + +use crate::event::ActionMap; + +/// Different kind of completions +#[derive(Clone, Default, Copy)] +pub enum InputCompleted { + #[default] + /// Complete a directory path in filesystem + Goto, + /// Complete a filename from current directory + Search, + /// Complete an executable name from $PATH + Exec, + /// Command + Command, +} + +impl fmt::Display for InputCompleted { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + #[rustfmt::skip] + Self::Exec => write!(f, "Exec: "), + #[rustfmt::skip] + Self::Goto => write!(f, "Goto: "), + #[rustfmt::skip] + Self::Search => write!(f, "Search: "), + #[rustfmt::skip] + Self::Command => write!(f, "Command: "), + } + } +} + +impl InputCompleted { + pub fn cursor_offset(&self) -> usize { + self.to_string().len() + 2 + } +} + +/// Holds a `Vec<String>` of possible completions and an `usize` index +/// showing where the user is in the vec. +#[derive(Clone, Default)] +pub struct Completion { + /// Possible completions + pub proposals: Vec<String>, + /// Which completion is selected by the user + pub index: usize, +} + +impl Completion { + /// Is there any completion option ? + pub fn is_empty(&self) -> bool { + self.proposals.is_empty() + } + + /// Move the index to next element, cycling to 0. + /// Does nothing if the list is empty. + pub fn next(&mut self) { + if self.proposals.is_empty() { + return; + } + self.index = (self.index + 1) % self.proposals.len() + } + + /// Move the index to previous element, cycling to the last one. + /// Does nothing if the list is empty. + pub fn prev(&mut self) { + if self.proposals.is_empty() { + return; + } + if self.index > 0 { + self.index -= 1 + } else { + self.index = self.proposals.len() - 1 + } + } + + /// Set the index to a new value if the value is below the length. + pub fn set_index(&mut self, index: usize) { + if index < self.proposals.len() { + self.index = index; + } + } + + /// Returns the currently selected proposition. + /// Returns an empty string if `proposals` is empty. + pub fn current_proposition(&self) -> &str { + if self.proposals.is_empty() { + return ""; + } + &self.proposals[self.index] + } + + /// Updates the proposition with a new `Vec`. + /// Reset the index to 0. + fn update(&mut self, proposals: Vec<String>) { + self.index = 0; + self.proposals = proposals; + self.proposals.dedup() + } + + fn extend(&mut self, proposals: &[String]) { + self.index = 0; + self.proposals.extend_from_slice(proposals); + self.proposals.dedup() + } + + /// Empty the proposals `Vec`. + /// Reset the index. + pub fn reset(&mut self) { + self.index = 0; + self.proposals.clear(); + } + + /// Goto completion. + /// Looks for the valid path completing what the user typed. + pub fn goto(&mut self, input_string: &str, current_path: &str) -> Result<()> { + self.goto_update_from_input(input_string, current_path); + let (parent, last_name) = split_input_string(input_string); + if last_name.is_empty() { + return Ok(()); + } + self.extend_absolute_paths(&parent, &last_name); + self.extend_relative_paths(current_path, &last_name); + Ok(()) + } + + fn goto_update_from_input(&mut self, input_string: &str, current_path: &str) { + self.proposals = vec![]; + if let Some(expanded_input) = self.expand_input(input_string) { + self.proposals.push(expanded_input); + } + if let Some(cannonicalized_input) = self.canonicalize_input(input_string, current_path) { + self.proposals.push(cannonicalized_input); + } + } + + fn expand_input(&mut self, input_string: &str) -> Option<String> { + let expanded_input = shellexpand::tilde(input_string).into_owned(); + if std::path::PathBuf::from(&expanded_input).exists() { + Some(expanded_input) + } else { + None + } + } + + fn canonicalize_input(&mut self, input_string: &str, current_path: &str) -> Option<String> { + let mut path = fs::canonicalize(current_path).unwrap(); + path.push(input_string); + let path = fs::canonicalize(path).unwrap_or_default(); + if path.exists() { + Some(path.to_str().unwrap_or_default().to_owned()) + } else { + None + } + } + + fn extend_absolute_paths(&mut self, parent: &str, last_name: &str) { + let Ok(path) = std::fs::canonicalize(parent) else { + return; + }; + let Ok(entries) = fs::read_dir(path) else { + return; + }; + self.extend(&Self::entries_matching_filename(entries, last_name)) + } + + fn extend_relative_paths(&mut self, current_path: &str, last_name: &str) { + if let Ok(entries) = fs::read_dir(current_path) { + self.extend(&Self::entries_matching_filename(entries, last_name)) + } + } + + fn entries_matching_filename(entries: ReadDir, last_name: &str) -> Vec<String> { + entries + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().unwrap().is_dir() && filename_startswith(e, last_name)) + .map(|e| e.path().to_string_lossy().into_owned()) + .collect() + } + + /// Looks for programs in $PATH completing the one typed by the user. + pub fn exec(&mut self, input_string: &str) -> Result<()> { + let mut proposals: Vec<String> = vec![]; + if let Some(paths) = std::env::var_os("PATH") { + for path in std::env::split_paths(&paths).filter(|path| path.exists()) { + proposals.extend(Self::find_completion_in_path(path, input_string)?); + } + } + self.update(proposals); + Ok(()) + } + + /// Looks for fm actions completing the one typed by the user. + pub fn command(&mut self, input_string: &str) -> Result<()> { + let proposals = ActionMap::iter() + .filter(|command| { + command + .to_string() + .to_lowercase() + .contains(&input_string.to_lowercase()) + }) + .map(|command| command.to_string()) + .collect(); + self.update(proposals); + Ok(()) + } + + fn find_completion_in_path( + path: std::path::PathBuf, + input_string: &str, + ) -> Result<Vec<String>> { + Ok(fs::read_dir(path)? + .filter_map(|e| e.ok()) + .filter(|e| file_match_input(e, input_string)) + .map(|e| e.path().to_string_lossy().into_owned()) + .collect()) + } + + /// Looks for file within current folder completing what the user typed. + pub fn search(&mut self, files: Vec<String>) { + self.update(files); + } + + /// Complete the input string with current_proposition if possible. + /// Returns the optional last chars of the current_proposition. + /// If the current_proposition doesn't start with input_string, it returns None. + pub fn complete_input_string(&self, input_string: &str) -> Option<&str> { + self.current_proposition().strip_prefix(input_string) + } + + /// Reverse the received effect if the index match the selected index. + pub fn attr(&self, index: usize, attr: &tuikit::attr::Attr) -> tuikit::attr::Attr { + let mut attr = *attr; + if index == self.index { + attr.effect |= tuikit::attr::Effect::REVERSE; + } + attr + } +} + +fn file_match_input(dir_entry: &std::fs::DirEntry, input_string: &str) -> bool { + let Ok(file_type) = dir_entry.file_type() else { + return false; + }; + (file_type.is_file() || file_type.is_symlink()) && filename_startswith(dir_entry, input_string) +} + +/// true if the filename starts with a pattern +fn filename_startswith(entry: &std::fs::DirEntry, pattern: &str) -> bool { + entry + .file_name() + .to_string_lossy() + .as_ref() + .starts_with(pattern) +} + +fn split_input_string(input_string: &str) -> (String, String) { + let steps = input_string.split('/'); + let mut vec_steps: Vec<&str> = steps.collect(); + let last_name = vec_steps.pop().unwrap_or("").to_owned(); + let parent = create_parent(vec_steps); + (parent, last_name) +} + +fn create_parent(vec_steps: Vec<&str>) -> String { + let mut parent = if vec_steps.is_empty() || vec_steps.len() == 1 && vec_steps[0] != "~" { + "/".to_owned() + } else { + "".to_owned() + }; + parent.push_str(&vec_steps.join("/")); + shellexpand::tilde(&parent).to_string() +} |