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:


![image](https://github.com/user-attachments/assets/8b668c21-0f3a-4d6f-9cd2-8cc92460525c)

### After:


![image](https://github.com/user-attachments/assets/ca491e64-774a-48d4-8f4f-58d647e011df)


![image](https://github.com/user-attachments/assets/4e097008-b5e1-4f63-af80-c1697025d4ad)



## User-Facing Changes

No

## Tests + Formatting

Passed

---------

Co-authored-by: blindfs <blindfs19@gmail.com>
This commit is contained in:
Nguyễn Hồng Quân
2025-02-18 00:52:07 +07:00
committed by GitHub
parent 2b8fb4fe00
commit 273226d666
3 changed files with 174 additions and 35 deletions

View File

@@ -1,20 +1,62 @@
pub mod support;
use std::{
fs::{read_dir, FileType, ReadDir},
path::{PathBuf, MAIN_SEPARATOR},
sync::Arc,
};
use nu_cli::NuCompleter;
use nu_engine::eval_block;
use nu_parser::parse;
use nu_path::expand_tilde;
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, PipelineData};
use reedline::{Completer, Suggestion};
use rstest::{fixture, rstest};
use std::{
path::{PathBuf, MAIN_SEPARATOR},
sync::Arc,
};
use support::{
completions_helpers::{new_dotnu_engine, new_partial_engine, new_quote_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]
fn completer() -> NuCompleter {
// Create a new engine
@@ -343,6 +385,57 @@ fn dotnu_completions() {
let suggestions = completer.complete(&completion_str, completion_str.len());
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]

View File

@@ -86,6 +86,23 @@ pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
// Add $nu
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
let mut stack = Stack::new();
@@ -95,17 +112,12 @@ pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) {
"TEST".to_string(),
Value::string("NUSHELL".to_string(), dir_span),
);
stack.add_env_var(
"NU_LIB_DIRS".to_string(),
Value::list(
vec![
Value::string(file(dir.join("lib-dir1")), dir_span),
Value::string(file(dir.join("lib-dir2")), dir_span),
Value::string(file(dir.join("lib-dir3")), dir_span),
],
dir_span,
),
"NU_LIB_DIRS".into(),
Value::test_list(vec![
Value::string(file(dir.join("lib-dir2")), dir_span),
Value::string(file(dir.join("lib-dir3")), dir_span),
]),
);
// Merge environment into the permanent state