2022-05-03 02:37:38 +02:00
|
|
|
use crate::completions::{matches, Completer, CompletionOptions};
|
2022-04-22 22:18:51 +02:00
|
|
|
use nu_protocol::{
|
|
|
|
engine::{EngineState, StateWorkingSet},
|
|
|
|
levenshtein_distance, Span,
|
|
|
|
};
|
|
|
|
use reedline::Suggestion;
|
2022-05-06 17:39:48 +02:00
|
|
|
use std::fs;
|
2022-04-22 22:18:51 +02:00
|
|
|
use std::path::Path;
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2022-05-03 02:37:38 +02:00
|
|
|
use super::{partial_from, prepend_base_dir, MatchAlgorithm};
|
|
|
|
|
2022-04-22 22:18:51 +02:00
|
|
|
const SEP: char = std::path::MAIN_SEPARATOR;
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct DirectoryCompletion {
|
|
|
|
engine_state: Arc<EngineState>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DirectoryCompletion {
|
|
|
|
pub fn new(engine_state: Arc<EngineState>) -> Self {
|
|
|
|
Self { engine_state }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Completer for DirectoryCompletion {
|
|
|
|
fn fetch(
|
|
|
|
&mut self,
|
|
|
|
_: &StateWorkingSet,
|
|
|
|
prefix: Vec<u8>,
|
|
|
|
span: Span,
|
|
|
|
offset: usize,
|
|
|
|
_: usize,
|
2022-04-23 17:01:19 +02:00
|
|
|
options: &CompletionOptions,
|
2022-04-22 22:18:51 +02:00
|
|
|
) -> Vec<Suggestion> {
|
2022-05-07 21:39:22 +02:00
|
|
|
let cwd = if let Some(d) = self.engine_state.get_env_var("PWD") {
|
2022-04-22 22:18:51 +02:00
|
|
|
match d.as_string() {
|
|
|
|
Ok(s) => s,
|
|
|
|
Err(_) => "".to_string(),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
"".to_string()
|
|
|
|
};
|
2022-04-23 17:01:19 +02:00
|
|
|
let partial = String::from_utf8_lossy(&prefix).to_string();
|
2022-04-22 22:18:51 +02:00
|
|
|
|
|
|
|
// Filter only the folders
|
2022-05-03 02:37:38 +02:00
|
|
|
let output: Vec<_> = directory_completion(span, &partial, &cwd, options.match_algorithm)
|
2022-04-22 22:18:51 +02:00
|
|
|
.into_iter()
|
2022-05-03 02:37:38 +02:00
|
|
|
.map(move |x| Suggestion {
|
|
|
|
value: x.1,
|
|
|
|
description: None,
|
|
|
|
extra: None,
|
|
|
|
span: reedline::Span {
|
|
|
|
start: x.0.start - offset,
|
|
|
|
end: x.0.end - offset,
|
|
|
|
},
|
|
|
|
append_whitespace: false,
|
2022-04-22 22:18:51 +02:00
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
output
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort results prioritizing the non hidden folders
|
|
|
|
fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
|
|
|
|
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
|
|
|
|
|
|
|
|
// Sort items
|
|
|
|
let mut sorted_items = items;
|
|
|
|
sorted_items.sort_by(|a, b| a.value.cmp(&b.value));
|
|
|
|
sorted_items.sort_by(|a, b| {
|
|
|
|
let a_distance = levenshtein_distance(&prefix_str, &a.value);
|
|
|
|
let b_distance = levenshtein_distance(&prefix_str, &b.value);
|
|
|
|
a_distance.cmp(&b_distance)
|
|
|
|
});
|
|
|
|
|
|
|
|
// Separate the results between hidden and non hidden
|
|
|
|
let mut hidden: Vec<Suggestion> = vec![];
|
|
|
|
let mut non_hidden: Vec<Suggestion> = vec![];
|
|
|
|
|
|
|
|
for item in sorted_items.into_iter() {
|
|
|
|
let item_path = Path::new(&item.value);
|
|
|
|
|
|
|
|
if let Some(value) = item_path.file_name() {
|
|
|
|
if let Some(value) = value.to_str() {
|
|
|
|
if value.starts_with('.') {
|
|
|
|
hidden.push(item);
|
|
|
|
} else {
|
|
|
|
non_hidden.push(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append the hidden folders to the non hidden vec to avoid creating a new vec
|
|
|
|
non_hidden.append(&mut hidden);
|
|
|
|
|
|
|
|
non_hidden
|
|
|
|
}
|
|
|
|
}
|
2022-05-03 02:37:38 +02:00
|
|
|
|
|
|
|
pub fn directory_completion(
|
|
|
|
span: nu_protocol::Span,
|
|
|
|
partial: &str,
|
|
|
|
cwd: &str,
|
|
|
|
match_algorithm: MatchAlgorithm,
|
|
|
|
) -> Vec<(nu_protocol::Span, String)> {
|
|
|
|
let original_input = partial;
|
|
|
|
|
|
|
|
let (base_dir_name, partial) = partial_from(partial);
|
|
|
|
|
|
|
|
let base_dir = nu_path::expand_path_with(&base_dir_name, cwd);
|
|
|
|
|
|
|
|
// This check is here as base_dir.read_dir() with base_dir == "" will open the current dir
|
|
|
|
// which we don't want in this case (if we did, base_dir would already be ".")
|
|
|
|
if base_dir == Path::new("") {
|
|
|
|
return Vec::new();
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Ok(result) = base_dir.read_dir() {
|
|
|
|
return result
|
|
|
|
.filter_map(|entry| {
|
|
|
|
entry.ok().and_then(|entry| {
|
2022-05-06 17:39:48 +02:00
|
|
|
if let Ok(metadata) = fs::metadata(entry.path()) {
|
2022-05-03 02:37:38 +02:00
|
|
|
if metadata.is_dir() {
|
|
|
|
let mut file_name = entry.file_name().to_string_lossy().into_owned();
|
|
|
|
if matches(&partial, &file_name, match_algorithm) {
|
|
|
|
let mut path = if prepend_base_dir(original_input, &base_dir_name) {
|
|
|
|
format!("{}{}", base_dir_name, file_name)
|
|
|
|
} else {
|
|
|
|
file_name.to_string()
|
|
|
|
};
|
|
|
|
|
|
|
|
if entry.path().is_dir() {
|
|
|
|
path.push(SEP);
|
|
|
|
file_name.push(SEP);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fix files or folders with quotes
|
|
|
|
if path.contains('\'') || path.contains('"') || path.contains(' ') {
|
|
|
|
path = format!("`{}`", path);
|
|
|
|
}
|
|
|
|
|
|
|
|
Some((span, path))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
}
|
|
|
|
|
|
|
|
Vec::new()
|
|
|
|
}
|