Repository: nushell/nushell (language: rust)

Issue:
`overlay use` does not work with directory autocomplete
### Describe the bug

`export use` seems to correctly allow for tab auto-completion of directory names, i.e. `export use scr` <kbd>Tab</kbd> becomes `export use Scripts\`

### How to reproduce

`>` `mkdir scripts`
`>` `touch scripts/activate.nu`
`>` `overlay use sc` <kbd>Tab</kbd>
`NO RECORDS FOUND`
`>` `overlay use scripts/ac` <kbd>Tab</kbd>
`NO RECORDS FOUND`

However, files in the cwd autocomplete properly.
`>` `cd scripts`
`>` `overlay use a` <kbd>Tab</kbd>
` ` `overlay use activate.nu` 

### Expected behavior

`>` `overlay use sc` <kbd>Tab</kbd>
`>` `overlay use scripts\`

### Configuration

| key                | value                                    |
| ------------------ | ---------------------------------------- |
| version            | 0.99.1                                   |
| major              | 0                                        |
| minor              | 99                                       |
| patch              | 1                                        |
| branch             |                                          |
| commit_hash        | 8812072f065be283df3e9a81f3945d0364f2f247 |
| build_os           | windows-x86_64                           |
| build_target       | x86_64-pc-windows-msvc                   |
| rust_version       | rustc 1.79.0 (129f3b996 2024-06-10)      |
| rust_channel       | 1.79.0-x86_64-pc-windows-msvc            |
| cargo_version      | cargo 1.79.0 (ffa9cf99a 2024-06-03)      |
| build_time         | 2024-10-20 19:44:43 +00:00               |
| build_rust_channel | release                                  |
| allocator          | mimalloc                                 |
| features           | default, sqlite, trash                   |
| installed_plugins  |                                          |


Discussion / hints:

Fix patch:
diff --git a/crates/nu-cli/src/completions/base.rs b/crates/nu-cli/src/completions/base.rs
index 68f10a7387183..359c2208b0f28 100644
--- a/crates/nu-cli/src/completions/base.rs
+++ b/crates/nu-cli/src/completions/base.rs
@@ -31,6 +31,7 @@ pub struct SemanticSuggestion {
 pub enum SuggestionKind {
     Command(nu_protocol::engine::CommandType),
     Type(nu_protocol::Type),
+    Module,
 }
 
 impl From<Suggestion> for SemanticSuggestion {
diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs
index 621219662786a..e45ff9cf978ad 100644
--- a/crates/nu-cli/src/completions/completion_common.rs
+++ b/crates/nu-cli/src/completions/completion_common.rs
@@ -157,7 +157,6 @@ pub struct FileSuggestion {
     pub span: nu_protocol::Span,
     pub path: String,
     pub style: Option<Style>,
-    pub cwd: PathBuf,
 }
 
 /// # Parameters
@@ -261,7 +260,6 @@ pub fn complete_item(
         if should_collapse_dots {
             p = collapse_ndots(p);
         }
-        let cwd = p.cwd.clone();
         let path = original_cwd.apply(p, path_separator);
         let style = ls_colors.as_ref().map(|lsc| {
             lsc.style_for_path_with_metadata(
@@ -277,7 +275,6 @@ pub fn complete_item(
             span,
             path: escape_path(path, want_directory),
             style,
-            cwd,
         }
     })
     .collect()
diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs
index 51fcd525fb2ba..c626e8b1d2afc 100644
--- a/crates/nu-cli/src/completions/dotnu_completions.rs
+++ b/crates/nu-cli/src/completions/dotnu_completions.rs
@@ -4,9 +4,9 @@ use nu_protocol::{
     Span,
 };
 use reedline::Suggestion;
-use std::path::{is_separator, Path, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
+use std::path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR};
 
-use super::SemanticSuggestion;
+use super::{SemanticSuggestion, SuggestionKind};
 
 #[derive(Clone, Default)]
 pub struct DotNuCompletion {}
@@ -29,19 +29,16 @@ impl Completer for DotNuCompletion {
         options: &CompletionOptions,
     ) -> Vec<SemanticSuggestion> {
         let prefix_str = String::from_utf8_lossy(prefix).replace('`', "");
-        let mut search_dirs: Vec<String> = 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) = prefix_str
             .rsplit_once(is_separator)
             .unwrap_or((".", &prefix_str));
         let base_dir = base.replace(is_separator, MAIN_SEPARATOR_STR);
-        let mut partial = partial.to_string();
-        // On windows, this standardizes paths to use \
-        let mut is_current_folder = false;
 
         // Fetch the lib dirs
-        let lib_dirs: Vec<String> = working_set
+        let lib_dirs: Vec<PathBuf> = 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"))
@@ -55,36 +52,27 @@ impl Completer for DotNuCompletion {
                                 .expect("internal error: failed to convert lib path")
                         })
                     })
-                    .map(|it| {
-                        it.into_os_string()
-                            .into_string()
-                            .expect("internal error: failed to convert OS path")
-                    })
                     .collect()
             })
             .unwrap_or_default();
 
         // Check if the base_dir is a folder
         // rsplit_once removes the separator
+        let cwd = working_set.permanent_state.cwd(None);
         if base_dir != "." {
-            // Add the base dir into the directories to be searched
-            search_dirs.push(base_dir.clone());
-
-            // Reset the partial adding the basic dir back
-            // in order to make the span replace work properly
-            let mut base_dir_partial = base_dir;
-            base_dir_partial.push_str(&partial);
-
-            partial = base_dir_partial;
+            // Search in base_dir as well as lib_dirs
+            if let Ok(mut cwd) = cwd {
+                cwd.push(&base_dir);
+                search_dirs.push(cwd.into_std_path_buf());
+            }
+            search_dirs.extend(lib_dirs.into_iter().map(|mut dir| {
+                dir.push(&base_dir);
+                dir
+            }));
         } else {
-            // Fetch the current folder
-            #[allow(deprecated)]
-            let current_folder = working_set.permanent_state.current_work_dir();
-            is_current_folder = true;
-
-            // Add the current folder and the lib dirs into the
-            // directories to be searched
-            search_dirs.push(current_folder);
+            if let Ok(cwd) = cwd {
+                search_dirs.push(cwd.into_std_path_buf());
+            }
             search_dirs.extend(lib_dirs);
         }
 
@@ -93,40 +81,39 @@ impl Completer for DotNuCompletion {
 
         let completions = file_path_completion(
             span,
-            &partial,
-            &search_dirs.iter().map(|d| d.as_str()).collect::<Vec<_>>(),
+            partial,
+            &search_dirs
+                .iter()
+                .map(|d| d.to_str().unwrap_or_default())
+                .collect::<Vec<_>>(),
             options,
             working_set.permanent_state,
             stack,
         );
         completions
             .into_iter()
-            .filter(move |it| {
-                // Different base dir, so we list the .nu files or folders
-                if !is_current_folder {
-                    it.path.ends_with(".nu") || it.path.ends_with(SEP)
+            // Different base dir, so we list the .nu files or folders
+            .filter(|it| it.path.ends_with(".nu") || it.path.ends_with(SEP))
+            .map(|x| {
+                let append_whitespace = x.path.ends_with(".nu");
+                // Re-calculate the span to replace
+                let span_offset = if base_dir == "." {
+                    0
                 } else {
-                    // Lib dirs, so we filter only the .nu files or directory modules
-                    if it.path.ends_with(SEP) {
-                        Path::new(&it.cwd).join(&it.path).join("mod.nu").exists()
-                    } else {
-                        it.path.ends_with(".nu")
-                    }
-                }
-            })
-            .map(move |x| SemanticSuggestion {
-                suggestion: Suggestion {
-                    value: x.path,
-                    style: x.style,
-                    span: reedline::Span {
-                        start: x.span.start - offset,
-                        end: x.span.end - offset,
+                    base_dir.len() + 1
+                } + prefix.iter().take_while(|c| **c == b'`').count();
+                let end = x.span.end - offset;
+                let start = std::cmp::min(end, x.span.start - offset + span_offset);
+                SemanticSuggestion {
+                    suggestion: Suggestion {
+                        value: x.path,
+                        style: x.style,
+                        span: reedline::Span { start, end },
+                        append_whitespace,
+                        ..Suggestion::default()
                     },
-                    append_whitespace: true,
-                    ..Suggestion::default()
-                },
-                // TODO????
-                kind: None,
+                    kind: Some(SuggestionKind::Module),
+                }
             })
             .collect::<Vec<_>>()
     }
diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs
index f053cfb9ac413..5ba35e75ce23d 100644
--- a/crates/nu-lsp/src/lib.rs
+++ b/crates/nu-lsp/src/lib.rs
@@ -668,6 +668,7 @@ impl LanguageServer {
                             .map(|kind| match kind {
                                 SuggestionKind::Type(t) => t.to_string(),
                                 SuggestionKind::Command(cmd) => cmd.to_string(),
+                                SuggestionKind::Module => "".to_string(),
                             })
                             .map(|s| CompletionItemLabelDetails {
                                 detail: None,
@@ -715,6 +716,7 @@ impl LanguageServer {
                 nu_protocol::engine::CommandType::Builtin => Some(CompletionItemKind::FUNCTION),
                 _ => None,
             },
+            SuggestionKind::Module => Some(CompletionItemKind::MODULE),
         })
     }
 }


Test patch:
diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs
index fa868a1d115de..d878d312d20c6 100644
--- a/crates/nu-cli/tests/completions/mod.rs
+++ b/crates/nu-cli/tests/completions/mod.rs
@@ -273,6 +273,15 @@ fn dotnu_completions() {
     // Instantiate a new completer
     let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack));
 
+    // Test nested nu script
+    #[cfg(windows)]
+    let completion_str = "use .\\dir_module\\sub_module\\".to_string();
+    #[cfg(not(windows))]
+    let completion_str = "use ./dir_module/sub_module/".to_string();
+    let suggestions = completer.complete(&completion_str, completion_str.len());
+
+    match_suggestions(&vec!["sub.nu".into()], &suggestions);
+
     let expected = vec![
         "asdf.nu".into(),
         "bar.nu".into(),
@@ -283,6 +292,18 @@ fn dotnu_completions() {
         #[cfg(not(windows))]
         "dir_module/".into(),
         "foo.nu".into(),
+        #[cfg(windows)]
+        "lib-dir1\\".into(),
+        #[cfg(not(windows))]
+        "lib-dir1/".into(),
+        #[cfg(windows)]
+        "lib-dir2\\".into(),
+        #[cfg(not(windows))]
+        "lib-dir2/".into(),
+        #[cfg(windows)]
+        "lib-dir3\\".into(),
+        #[cfg(not(windows))]
+        "lib-dir3/".into(),
         "spam.nu".into(),
         "xyzzy.nu".into(),
     ];
diff --git a/tests/fixtures/dotnu_completions/dir_module/sub_module/sub.nu b/tests/fixtures/dotnu_completions/dir_module/sub_module/sub.nu
new file mode 100644
index 0000000000000..e69de29bb2d1d