mirror of
https://github.com/nushell/nushell.git
synced 2025-04-30 16:14:27 +02:00
Provide more directories autocomplete for "overlay use" (#15057)
## Description - Fixes #12891 - An escape for #12835 Currently, Nushell is not very friendly to Python workflow, because Python developers very often need to activate a virtual environment, and in some workflow, the _activate.nu_ script is not near to "current working directory" (especially ones who use [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/) and [Poetry](https://python-poetry.org/)), and hence, is not auto-completed for `overlay use`. Though Nu v0.102.0 has improved auto-complete for `overlay use`, it doesn't work if user starts the file path with `~` or `/`, like: ```nu > overlay use /h ``` ```nu > overlay use ~/W ``` ### Before:  ### After:   ## User-Facing Changes No ## Tests + Formatting Passed --------- Co-authored-by: blindfs <blindfs19@gmail.com>
This commit is contained in:
parent
2b8fb4fe00
commit
273226d666
@ -5,7 +5,10 @@ use nu_protocol::{
|
|||||||
Span,
|
Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{SemanticSuggestion, SuggestionKind};
|
use super::{SemanticSuggestion, SuggestionKind};
|
||||||
|
|
||||||
@ -33,47 +36,76 @@ impl Completer for DotNuCompletion {
|
|||||||
let start_with_backquote = prefix_str.starts_with('`');
|
let start_with_backquote = prefix_str.starts_with('`');
|
||||||
let end_with_backquote = prefix_str.ends_with('`');
|
let end_with_backquote = prefix_str.ends_with('`');
|
||||||
let prefix_str = prefix_str.replace('`', "");
|
let prefix_str = prefix_str.replace('`', "");
|
||||||
|
// e.g. `./`, `..\`, `/`
|
||||||
|
let not_lib_dirs = prefix_str
|
||||||
|
.chars()
|
||||||
|
.find(|c| *c != '.')
|
||||||
|
.is_some_and(is_separator);
|
||||||
let mut search_dirs: Vec<PathBuf> = vec![];
|
let mut search_dirs: Vec<PathBuf> = vec![];
|
||||||
|
|
||||||
// If prefix_str is only a word we want to search in the current dir
|
let (base, partial) = if let Some((parent, remain)) = prefix_str.rsplit_once(is_separator) {
|
||||||
let (base, partial) = prefix_str
|
// If prefix_str is only a word we want to search in the current dir.
|
||||||
.rsplit_once(is_separator)
|
// "/xx" should be split to "/" and "xx".
|
||||||
.unwrap_or((".", &prefix_str));
|
if parent.is_empty() {
|
||||||
|
(MAIN_SEPARATOR_STR, remain)
|
||||||
|
} else {
|
||||||
|
(parent, remain)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(".", prefix_str.as_str())
|
||||||
|
};
|
||||||
let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
|
let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
|
||||||
|
|
||||||
// Fetch the lib dirs
|
// Fetch the lib dirs
|
||||||
let lib_dirs: Vec<PathBuf> = working_set
|
// NOTE: 2 ways to setup `NU_LIB_DIRS`
|
||||||
|
// 1. `const NU_LIB_DIRS = [paths]`, equal to `nu -I paths`
|
||||||
|
// 2. `$env.NU_LIB_DIRS = [paths]`
|
||||||
|
let const_lib_dirs = working_set
|
||||||
.find_variable(b"$NU_LIB_DIRS")
|
.find_variable(b"$NU_LIB_DIRS")
|
||||||
.and_then(|vid| working_set.get_variable(vid).const_val.as_ref())
|
.and_then(|vid| working_set.get_variable(vid).const_val.as_ref());
|
||||||
.or(working_set.get_env_var("NU_LIB_DIRS"))
|
let env_lib_dirs = working_set.get_env_var("NU_LIB_DIRS");
|
||||||
.map(|lib_dirs| {
|
let lib_dirs: HashSet<PathBuf> = [const_lib_dirs, env_lib_dirs]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.flat_map(|lib_dirs| {
|
||||||
lib_dirs
|
lib_dirs
|
||||||
.as_list()
|
.as_list()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|it| it.iter().filter_map(|x| x.to_path().ok()))
|
.flat_map(|it| it.iter().filter_map(|x| x.to_path().ok()))
|
||||||
.map(expand_tilde)
|
.map(expand_tilde)
|
||||||
.collect()
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.collect();
|
||||||
|
|
||||||
// Check if the base_dir is a folder
|
// Check if the base_dir is a folder
|
||||||
// rsplit_once removes the separator
|
|
||||||
let cwd = working_set.permanent_state.cwd(None);
|
let cwd = working_set.permanent_state.cwd(None);
|
||||||
if base_dir != "." {
|
if base_dir != "." {
|
||||||
// Search in base_dir as well as lib_dirs
|
let expanded_base_dir = expand_tilde(&base_dir);
|
||||||
|
let is_base_dir_relative = expanded_base_dir.is_relative();
|
||||||
|
// Search in base_dir as well as lib_dirs.
|
||||||
|
// After expanded, base_dir can be a relative path or absolute path.
|
||||||
|
// If relative, we join "current working dir" with it to get subdirectory and add to search_dirs.
|
||||||
|
// If absolute, we add it to search_dirs.
|
||||||
if let Ok(mut cwd) = cwd {
|
if let Ok(mut cwd) = cwd {
|
||||||
cwd.push(&base_dir);
|
if is_base_dir_relative {
|
||||||
search_dirs.push(cwd.into_std_path_buf());
|
cwd.push(&base_dir);
|
||||||
|
search_dirs.push(cwd.into_std_path_buf());
|
||||||
|
} else {
|
||||||
|
search_dirs.push(expanded_base_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !not_lib_dirs {
|
||||||
|
search_dirs.extend(lib_dirs.into_iter().map(|mut dir| {
|
||||||
|
dir.push(&base_dir);
|
||||||
|
dir
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
search_dirs.extend(lib_dirs.into_iter().map(|mut dir| {
|
|
||||||
dir.push(&base_dir);
|
|
||||||
dir
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
if let Ok(cwd) = cwd {
|
if let Ok(cwd) = cwd {
|
||||||
search_dirs.push(cwd.into_std_path_buf());
|
search_dirs.push(cwd.into_std_path_buf());
|
||||||
}
|
}
|
||||||
search_dirs.extend(lib_dirs);
|
if !not_lib_dirs {
|
||||||
|
search_dirs.extend(lib_dirs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the files filtering the ones that ends with .nu
|
// Fetch the files filtering the ones that ends with .nu
|
||||||
@ -104,7 +136,9 @@ impl Completer for DotNuCompletion {
|
|||||||
let mut span_offset = 0;
|
let mut span_offset = 0;
|
||||||
let mut value = x.path.to_string();
|
let mut value = x.path.to_string();
|
||||||
// Complete only the last path component
|
// Complete only the last path component
|
||||||
if base_dir != "." {
|
if base_dir == MAIN_SEPARATOR_STR {
|
||||||
|
span_offset = base_dir.len()
|
||||||
|
} else if base_dir != "." {
|
||||||
span_offset = base_dir.len() + 1
|
span_offset = base_dir.len() + 1
|
||||||
}
|
}
|
||||||
// Retain only one '`'
|
// Retain only one '`'
|
||||||
|
@ -1,20 +1,62 @@
|
|||||||
pub mod support;
|
pub mod support;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::{read_dir, FileType, ReadDir},
|
||||||
|
path::{PathBuf, MAIN_SEPARATOR},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use nu_cli::NuCompleter;
|
use nu_cli::NuCompleter;
|
||||||
use nu_engine::eval_block;
|
use nu_engine::eval_block;
|
||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
|
use nu_path::expand_tilde;
|
||||||
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
|
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
|
||||||
use reedline::{Completer, Suggestion};
|
use reedline::{Completer, Suggestion};
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use std::{
|
|
||||||
path::{PathBuf, MAIN_SEPARATOR},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use support::{
|
use support::{
|
||||||
completions_helpers::{new_dotnu_engine, new_partial_engine, new_quote_engine},
|
completions_helpers::{new_dotnu_engine, new_partial_engine, new_quote_engine},
|
||||||
file, folder, match_suggestions, new_engine,
|
file, folder, match_suggestions, new_engine,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Match a list of suggestions with the content of a directory.
|
||||||
|
// This helper is for DotNutCompletion, so actually it only retrieves
|
||||||
|
// *.nu files and subdirectories.
|
||||||
|
pub fn match_dir_content_for_dotnu(dir: ReadDir, suggestions: &[Suggestion]) {
|
||||||
|
let actual_dir_entries: Vec<_> = dir.filter_map(|c| c.ok()).collect();
|
||||||
|
let type_name_pairs: Vec<(FileType, String)> = actual_dir_entries
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|t| t.file_type().ok().zip(t.file_name().into_string().ok()))
|
||||||
|
.collect();
|
||||||
|
let mut simple_dir_entries: Vec<&str> = type_name_pairs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(t, n)| {
|
||||||
|
if t.is_dir() || n.ends_with(".nu") {
|
||||||
|
Some(n.as_str())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
simple_dir_entries.sort();
|
||||||
|
let mut pure_suggestions: Vec<&str> = suggestions
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
// The file names in suggestions contain some extra characters,
|
||||||
|
// we clean them to compare more exactly with read_dir result.
|
||||||
|
s.value
|
||||||
|
.as_str()
|
||||||
|
.trim_end_matches('`')
|
||||||
|
.trim_end_matches('/')
|
||||||
|
.trim_end_matches('\\')
|
||||||
|
.trim_start_matches('`')
|
||||||
|
.trim_start_matches("~/")
|
||||||
|
.trim_start_matches("~\\")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
pure_suggestions.sort();
|
||||||
|
assert_eq!(simple_dir_entries, pure_suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
fn completer() -> NuCompleter {
|
fn completer() -> NuCompleter {
|
||||||
// Create a new engine
|
// Create a new engine
|
||||||
@ -343,6 +385,57 @@ fn dotnu_completions() {
|
|||||||
let suggestions = completer.complete(&completion_str, completion_str.len());
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
|
||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
|
|
||||||
|
// Test special paths
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let completion_str = "use \\".to_string();
|
||||||
|
let dir_content = read_dir("\\").unwrap();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
match_dir_content_for_dotnu(dir_content, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let completion_str = "use /".to_string();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
let dir_content = read_dir("/").unwrap();
|
||||||
|
match_dir_content_for_dotnu(dir_content, &suggestions);
|
||||||
|
|
||||||
|
let completion_str = "use ~".to_string();
|
||||||
|
let dir_content = read_dir(expand_tilde("~")).unwrap();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
match_dir_content_for_dotnu(dir_content, &suggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dotnu_completions_const_nu_lib_dirs() {
|
||||||
|
let (_, _, engine, stack) = new_dotnu_engine();
|
||||||
|
let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
|
||||||
|
|
||||||
|
// file in `lib-dir1/`, set by `const NU_LIB_DIRS`
|
||||||
|
let completion_str = "use xyzz".to_string();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["xyzzy.nu".into()], &suggestions);
|
||||||
|
|
||||||
|
// file in `lib-dir2/`, set by `$env.NU_LIB_DIRS`
|
||||||
|
let completion_str = "use asdf".to_string();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["asdf.nu".into()], &suggestions);
|
||||||
|
|
||||||
|
// file in `lib-dir3/`, set by both, should not replicate
|
||||||
|
let completion_str = "use spam".to_string();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
match_suggestions(&vec!["spam.nu".into()], &suggestions);
|
||||||
|
|
||||||
|
// if `./` specified by user, file in `lib-dir*` should be ignored
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let completion_str = "use .\\asdf".to_string();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
assert!(suggestions.is_empty());
|
||||||
|
}
|
||||||
|
let completion_str = "use ./asdf".to_string();
|
||||||
|
let suggestions = completer.complete(&completion_str, completion_str.len());
|
||||||
|
assert!(suggestions.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -86,6 +86,23 @@ pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
|||||||
// Add $nu
|
// Add $nu
|
||||||
engine_state.generate_nu_constant();
|
engine_state.generate_nu_constant();
|
||||||
|
|
||||||
|
// const $NU_LIB_DIRS
|
||||||
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
|
let var_id = working_set.add_variable(
|
||||||
|
b"$NU_LIB_DIRS".into(),
|
||||||
|
Span::unknown(),
|
||||||
|
nu_protocol::Type::List(Box::new(nu_protocol::Type::String)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
working_set.set_variable_const_val(
|
||||||
|
var_id,
|
||||||
|
Value::test_list(vec![
|
||||||
|
Value::string(file(dir.join("lib-dir1")), dir_span),
|
||||||
|
Value::string(file(dir.join("lib-dir3")), dir_span),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
let _ = engine_state.merge_delta(working_set.render());
|
||||||
|
|
||||||
// New stack
|
// New stack
|
||||||
let mut stack = Stack::new();
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
@ -95,17 +112,12 @@ pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
|
|||||||
"TEST".to_string(),
|
"TEST".to_string(),
|
||||||
Value::string("NUSHELL".to_string(), dir_span),
|
Value::string("NUSHELL".to_string(), dir_span),
|
||||||
);
|
);
|
||||||
|
|
||||||
stack.add_env_var(
|
stack.add_env_var(
|
||||||
"NU_LIB_DIRS".to_string(),
|
"NU_LIB_DIRS".into(),
|
||||||
Value::list(
|
Value::test_list(vec![
|
||||||
vec![
|
Value::string(file(dir.join("lib-dir2")), dir_span),
|
||||||
Value::string(file(dir.join("lib-dir1")), dir_span),
|
Value::string(file(dir.join("lib-dir3")), dir_span),
|
||||||
Value::string(file(dir.join("lib-dir2")), dir_span),
|
]),
|
||||||
Value::string(file(dir.join("lib-dir3")), dir_span),
|
|
||||||
],
|
|
||||||
dir_span,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Merge environment into the permanent state
|
// Merge environment into the permanent state
|
||||||
|
Loading…
Reference in New Issue
Block a user