use nu_engine::value_shell::ValueShell; use nu_protocol::ColumnPath; use nu_source::SpannedItem; use super::matchers::Matcher; use crate::{Completer, CompletionContext, Suggestion}; use std::path::{Path, PathBuf}; fn build_path(head: &str, members: &Path, entry: &str) -> String { let mut full_path = head.to_string(); full_path.push_str( &members .join(entry) .display() .to_string() .replace(std::path::MAIN_SEPARATOR, "."), ); full_path } fn collect_entries(value_fs: &ValueShell, head: &str, path: &Path) -> Vec { value_fs .members_under(path) .iter() .flat_map(|entry| { entry .row_entries() .map(|(entry_name, _)| build_path(head, path, entry_name)) }) .collect() } pub struct VariableCompleter; impl Completer for VariableCompleter where Context: CompletionContext, { fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { let registry = ctx.variable_registry(); let variables_available = registry.variables(); let partial_column_path = ColumnPath::with_head(&partial.to_string().spanned_unknown()); partial_column_path .map(|(head, members)| { variables_available .iter() .filter(|candidate| matcher.matches(&head, candidate)) .into_iter() .filter_map(|candidate| { if !partial.ends_with('.') && members.is_empty() { Some(vec![candidate.to_string()]) } else { let value = registry.get_variable(&candidate[..].spanned_unknown()); let path = PathBuf::from(members.path()); value.map(|candidate| { let fs = ValueShell::new(candidate); fs.find(&path) .map(|fs| collect_entries(fs, &head, &path)) .or_else(|| { path.parent().map(|parent| { fs.find(parent) .map(|fs| collect_entries(fs, &head, parent)) .unwrap_or_default() }) }) .unwrap_or_default() }) } }) .flatten() .filter_map(|candidate| { if matcher.matches(partial, &candidate) { Some(Suggestion::new(&candidate, &candidate)) } else { None } }) .collect() }) .unwrap_or_default() } } #[cfg(test)] mod tests { use super::{Completer, Suggestion as S, VariableCompleter}; use crate::matchers::case_insensitive::Matcher as CaseInsensitiveMatcher; use nu_engine::EnvVar; use nu_engine::{ evaluation_context::EngineState, ConfigHolder, EvaluationContext, FakeHost, Host, Scope, ShellManager, }; use nu_protocol::{SignatureRegistry, VariableRegistry}; use parking_lot::Mutex; use std::ffi::OsString; use std::sync::{atomic::AtomicBool, Arc}; struct CompletionContext<'a>(&'a EvaluationContext); impl<'a> crate::CompletionContext for CompletionContext<'a> { fn signature_registry(&self) -> &dyn SignatureRegistry { &self.0.scope } fn source(&self) -> &nu_engine::EvaluationContext { self.0 } fn scope(&self) -> &dyn nu_parser::ParserScope { &self.0.scope } fn variable_registry(&self) -> &dyn VariableRegistry { self.0 } } fn create_context_with_host(host: Box) -> EvaluationContext { let scope = Scope::new(); let env_vars = host .vars() .iter() .cloned() .map(|(k, v)| (k, EnvVar::from(v))) .collect(); scope.add_env(env_vars); EvaluationContext { scope, engine_state: Arc::new(EngineState { host: Arc::new(parking_lot::Mutex::new(host)), current_errors: Arc::new(Mutex::new(vec![])), ctrl_c: Arc::new(AtomicBool::new(false)), configs: Arc::new(Mutex::new(ConfigHolder::new())), shell_manager: ShellManager::basic(), windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())), }), } } fn set_envs(host: &mut FakeHost, values: Vec<(&str, &str)>) { values.iter().for_each(|(key, value)| { host.env_set(OsString::from(key), OsString::from(value)); }); } #[test] fn structure() { let mut host = nu_engine::FakeHost::new(); set_envs(&mut host, vec![("COMPLETER", "VARIABLE"), ("SHELL", "NU")]); let context = create_context_with_host(Box::new(host)); assert_eq!( VariableCompleter {}.complete( &CompletionContext(&context), "$nu.env.", &CaseInsensitiveMatcher ), vec![ S::new("$nu.env.COMPLETER", "$nu.env.COMPLETER"), S::new("$nu.env.SHELL", "$nu.env.SHELL") ] ); assert_eq!( VariableCompleter {}.complete( &CompletionContext(&context), "$nu.env.CO", &CaseInsensitiveMatcher ), vec![S::new("$nu.env.COMPLETER", "$nu.env.COMPLETER"),] ); assert_eq!( VariableCompleter {}.complete( &CompletionContext(&context), "$nu.en", &CaseInsensitiveMatcher ), vec![S::new("$nu.env", "$nu.env"),] ); } }