Repository: nushell/nushell (language: rust)

Issue:
Completion for overlay use
### Related problem

_No response_

### Describe the solution you'd like

after input `overlay use` on the shell, we can still use path completion
for example, when I'm using python's venv, I can input
```nushell
overlay use ./.venv/
```
and then tab to completion `./.venv/Scripts/activate.nu`

### Describe alternatives you've considered

_No response_

### Additional context and details

_No response_


Discussion / hints:

Fix patch:
diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs
index f06b85f5ed6c0..7bb3458fe04c9 100644
--- a/crates/nu-cli/src/completions/dotnu_completions.rs
+++ b/crates/nu-cli/src/completions/dotnu_completions.rs
@@ -5,7 +5,10 @@ use nu_protocol::{
     Span,
 };
 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};
 
@@ -33,47 +36,76 @@ impl Completer for DotNuCompletion {
         let start_with_backquote = prefix_str.starts_with('`');
         let end_with_backquote = prefix_str.ends_with('`');
         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![];
 
-        // If prefix_str is only a word we want to search in the current dir
-        let (base, partial) = prefix_str
-            .rsplit_once(is_separator)
-            .unwrap_or((".", &prefix_str));
+        let (base, partial) = if let Some((parent, remain)) = prefix_str.rsplit_once(is_separator) {
+            // If prefix_str is only a word we want to search in the current dir.
+            // "/xx" should be split to "/" and "xx".
+            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);
 
         // 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")
-            .and_then(|vid| working_set.get_variable(vid).const_val.as_ref())
-            .or(working_set.get_env_var("NU_LIB_DIRS"))
-            .map(|lib_dirs| {
+            .and_then(|vid| working_set.get_variable(vid).const_val.as_ref());
+        let env_lib_dirs = working_set.get_env_var("NU_LIB_DIRS");
+        let lib_dirs: HashSet<PathBuf> = [const_lib_dirs, env_lib_dirs]
+            .into_iter()
+            .flatten()
+            .flat_map(|lib_dirs| {
                 lib_dirs
                     .as_list()
                     .into_iter()
                     .flat_map(|it| it.iter().filter_map(|x| x.to_path().ok()))
                     .map(expand_tilde)
-                    .collect()
             })
-            .unwrap_or_default();
+            .collect();
 
         // Check if the base_dir is a folder
-        // rsplit_once removes the separator
         let cwd = working_set.permanent_state.cwd(None);
         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 {
-                cwd.push(&base_dir);
-                search_dirs.push(cwd.into_std_path_buf());
+                if is_base_dir_relative {
+                    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 {
             if let Ok(cwd) = cwd {
                 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
@@ -104,7 +136,9 @@ impl Completer for DotNuCompletion {
                 let mut span_offset = 0;
                 let mut value = x.path.to_string();
                 // 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
                 }
                 // Retain only one '`'


Test patch:
diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs
index 3e07a6e2662b9..dfc8f3d679cb6 100644
--- a/crates/nu-cli/tests/completions/mod.rs
+++ b/crates/nu-cli/tests/completions/mod.rs
@@ -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]
diff --git a/crates/nu-cli/tests/completions/support/completions_helpers.rs b/crates/nu-cli/tests/completions/support/completions_helpers.rs
index 83e4dfddf3dad..ed541997a3a61 100644
--- a/crates/nu-cli/tests/completions/support/completions_helpers.rs
+++ b/crates/nu-cli/tests/completions/support/completions_helpers.rs
@@ -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