diff --git a/Cargo.lock b/Cargo.lock index 87a35ff52a..5bdaf61196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3542,6 +3542,7 @@ dependencies = [ "nu-path", "nu-plugin-engine", "nu-protocol", + "nu-std", "nu-test-support", "nu-utils", "nucleo-matcher", @@ -3827,6 +3828,7 @@ dependencies = [ "nu-glob", "nu-parser", "nu-protocol", + "nu-std", "nu-test-support", "nu-utils", "nucleo-matcher", diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index f3bfb0f91c..0909caf125 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -13,6 +13,7 @@ bench = false [dev-dependencies] nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" } nu-command = { path = "../nu-command", version = "0.103.1" } +nu-std = { path = "../nu-std", version = "0.103.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.1" } rstest = { workspace = true, default-features = false } tempfile = { workspace = true } diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index a7a00306b0..bb56963942 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -1,21 +1,20 @@ use crate::completions::{ + base::{SemanticSuggestion, SuggestionKind}, AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer, - CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion, - FlagCompletion, OperatorCompletion, VariableCompletion, + CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, + ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion, }; use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; use nu_engine::eval_block; -use nu_parser::{flatten_expression, parse}; +use nu_parser::{flatten_expression, parse, parse_module_file_or_dir}; use nu_protocol::{ - ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse}, + ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse}, debugger::WithoutDebug, engine::{Closure, EngineState, Stack, StateWorkingSet}, PipelineData, Span, Type, Value, }; use reedline::{Completer as ReedlineCompleter, Suggestion}; -use std::{str, sync::Arc}; - -use super::base::{SemanticSuggestion, SuggestionKind}; +use std::sync::Arc; /// Used as the function `f` in find_map Traverse /// @@ -57,8 +56,13 @@ fn find_pipeline_element_by_position<'a>( Expr::FullCellPath(fcp) => fcp .head .find_map(working_set, &closure) - .or(Some(expr)) .map(FindMapResult::Found) + // e.g. use std/util [ + .or_else(|| { + (fcp.head.span.contains(pos) && matches!(fcp.head.expr, Expr::List(_))) + .then_some(FindMapResult::Continue) + }) + .or(Some(FindMapResult::Found(expr))) .unwrap_or_default(), Expr::Var(_) => FindMapResult::Found(expr), Expr::AttributeBlock(ab) => ab @@ -127,6 +131,18 @@ struct Context<'a> { offset: usize, } +/// For argument completion +struct PositionalArguments<'a> { + /// command name + command_head: &'a [u8], + /// indices of positional arguments + positional_arg_indices: Vec, + /// argument list + arguments: &'a [Argument], + /// expression of current argument + expr: &'a Expression, +} + impl Context<'_> { fn new<'a>( working_set: &'a StateWorkingSet, @@ -328,7 +344,8 @@ impl NuCompleter { // NOTE: the argument to complete is not necessarily the last one // for lsp completion, we don't trim the text, // so that `def`s after pos can be completed - for arg in call.arguments.iter() { + let mut positional_arg_indices = Vec::new(); + for (arg_idx, arg) in call.arguments.iter().enumerate() { let span = arg.span(); if span.contains(pos) { // if customized completion specified, it has highest priority @@ -379,9 +396,15 @@ impl NuCompleter { // complete according to expression type and command head Argument::Positional(expr) => { let command_head = working_set.get_span_contents(call.head); + positional_arg_indices.push(arg_idx); self.argument_completion_helper( - command_head, - expr, + PositionalArguments { + command_head, + positional_arg_indices, + arguments: &call.arguments, + expr, + }, + pos, &ctx, suggestions.is_empty(), ) @@ -389,6 +412,8 @@ impl NuCompleter { _ => vec![], }); break; + } else if !matches!(arg, Argument::Named(_)) { + positional_arg_indices.push(arg_idx); } } } @@ -498,18 +523,95 @@ impl NuCompleter { fn argument_completion_helper( &self, - command_head: &[u8], - expr: &Expression, + argument_info: PositionalArguments, + pos: usize, ctx: &Context, need_fallback: bool, ) -> Vec { + let PositionalArguments { + command_head, + positional_arg_indices, + arguments, + expr, + } = argument_info; // special commands match command_head { // complete module file/directory - // TODO: if module file already specified, + b"use" | b"export use" | b"overlay use" | b"source-env" + if positional_arg_indices.len() == 1 => + { + return self.process_completion( + &mut DotNuCompletion { + std_virtual_path: command_head != b"source-env", + }, + ctx, + ); + } + // NOTE: if module file already specified, // should parse it to get modules/commands/consts to complete - b"use" | b"export use" | b"overlay use" | b"source-env" => { - return self.process_completion(&mut DotNuCompletion, ctx); + b"use" | b"export use" => { + let Some(Argument::Positional(Expression { + expr: Expr::String(module_name), + span, + .. + })) = positional_arg_indices + .first() + .and_then(|i| arguments.get(*i)) + else { + return vec![]; + }; + let module_name = module_name.as_bytes(); + let (module_id, temp_working_set) = match ctx.working_set.find_module(module_name) { + Some(module_id) => (module_id, None), + None => { + let mut temp_working_set = + StateWorkingSet::new(ctx.working_set.permanent_state); + let Some(module_id) = parse_module_file_or_dir( + &mut temp_working_set, + module_name, + *span, + None, + ) else { + return vec![]; + }; + (module_id, Some(temp_working_set)) + } + }; + let mut exportable_completion = ExportableCompletion { + module_id, + temp_working_set, + }; + let mut complete_on_list_items = |items: &[ListItem]| -> Vec { + for item in items { + let span = item.expr().span; + if span.contains(pos) { + let offset = span.start.saturating_sub(ctx.span.start); + let end_offset = + ctx.prefix.len().min(pos.min(span.end) - ctx.span.start + 1); + let new_ctx = Context::new( + ctx.working_set, + Span::new(span.start, ctx.span.end.min(span.end)), + ctx.prefix.get(offset..end_offset).unwrap_or_default(), + ctx.offset, + ); + return self.process_completion(&mut exportable_completion, &new_ctx); + } + } + vec![] + }; + + match &expr.expr { + Expr::String(_) => { + return self.process_completion(&mut exportable_completion, ctx); + } + Expr::FullCellPath(fcp) => match &fcp.head.expr { + Expr::List(items) => { + return complete_on_list_items(items); + } + _ => return vec![], + }, + _ => return vec![], + } } b"which" => { let mut completer = CommandCompletion { diff --git a/crates/nu-cli/src/completions/completion_common.rs b/crates/nu-cli/src/completions/completion_common.rs index e8179e0ec9..9e8e8fe119 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -150,7 +150,7 @@ impl OriginalCwd { } } -fn surround_remove(partial: &str) -> String { +pub fn surround_remove(partial: &str) -> String { for c in ['`', '"', '\''] { if partial.starts_with(c) { let ret = partial.strip_prefix(c).unwrap_or(partial); diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs index 084d52d65b..06cf4237fb 100644 --- a/crates/nu-cli/src/completions/dotnu_completions.rs +++ b/crates/nu-cli/src/completions/dotnu_completions.rs @@ -1,18 +1,23 @@ -use crate::completions::{file_path_completion, Completer, CompletionOptions}; +use crate::completions::{ + completion_common::{surround_remove, FileSuggestion}, + completion_options::NuMatcher, + file_path_completion, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, +}; use nu_path::expand_tilde; use nu_protocol::{ - engine::{Stack, StateWorkingSet}, + engine::{Stack, StateWorkingSet, VirtualPath}, Span, }; use reedline::Suggestion; use std::{ collections::HashSet, - path::{is_separator, PathBuf, MAIN_SEPARATOR as SEP, MAIN_SEPARATOR_STR}, + path::{is_separator, PathBuf, MAIN_SEPARATOR_STR}, }; -use super::{SemanticSuggestion, SuggestionKind}; - -pub struct DotNuCompletion; +pub struct DotNuCompletion { + /// e.g. use std/a + pub std_virtual_path: bool, +} impl Completer for DotNuCompletion { fn fetch( @@ -102,7 +107,7 @@ impl Completer for DotNuCompletion { // Fetch the files filtering the ones that ends with .nu // and transform them into suggestions - let completions = file_path_completion( + let mut completions = file_path_completion( span, partial, &search_dirs @@ -113,17 +118,60 @@ impl Completer for DotNuCompletion { working_set.permanent_state, stack, ); + + if self.std_virtual_path { + let mut matcher = NuMatcher::new(partial, options); + let base_dir = surround_remove(&base_dir); + if base_dir == "." { + let surround_prefix = partial + .chars() + .take_while(|c| "`'\"".contains(*c)) + .collect::(); + for path in ["std", "std-rfc"] { + let path = format!("{}{}", surround_prefix, path); + matcher.add( + path.clone(), + FileSuggestion { + span, + path, + style: None, + is_dir: true, + }, + ); + } + } else if let Some(VirtualPath::Dir(sub_paths)) = + working_set.find_virtual_path(&base_dir) + { + for sub_vp_id in sub_paths { + let (path, sub_vp) = working_set.get_virtual_path(*sub_vp_id); + let path = path + .strip_prefix(&format!("{}/", base_dir)) + .unwrap_or(path) + .to_string(); + matcher.add( + path.clone(), + FileSuggestion { + path, + span, + style: None, + is_dir: matches!(sub_vp, VirtualPath::Dir(_)), + }, + ); + } + } + completions.extend(matcher.results()); + } + completions .into_iter() // Different base dir, so we list the .nu files or folders .filter(|it| { // for paths with spaces in them let path = it.path.trim_end_matches('`'); - path.ends_with(".nu") || path.ends_with(SEP) + path.ends_with(".nu") || it.is_dir }) .map(|x| { - let append_whitespace = - x.path.ends_with(".nu") && (!start_with_backquote || end_with_backquote); + let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote); // Re-calculate the span to replace let mut span_offset = 0; let mut value = x.path.to_string(); diff --git a/crates/nu-cli/src/completions/exportable_completions.rs b/crates/nu-cli/src/completions/exportable_completions.rs new file mode 100644 index 0000000000..a375b2556c --- /dev/null +++ b/crates/nu-cli/src/completions/exportable_completions.rs @@ -0,0 +1,111 @@ +use crate::completions::{ + completion_common::surround_remove, completion_options::NuMatcher, Completer, + CompletionOptions, SemanticSuggestion, SuggestionKind, +}; +use nu_protocol::{ + engine::{Stack, StateWorkingSet}, + ModuleId, Span, +}; +use reedline::Suggestion; + +pub struct ExportableCompletion<'a> { + pub module_id: ModuleId, + pub temp_working_set: Option>, +} + +/// If name contains space, wrap it in quotes +fn wrapped_name(name: String) -> String { + if !name.contains(' ') { + return name; + } + if name.contains('\'') { + format!("\"{}\"", name.replace('"', r#"\""#)) + } else { + format!("'{name}'") + } +} + +impl Completer for ExportableCompletion<'_> { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + _stack: &Stack, + prefix: impl AsRef, + span: Span, + offset: usize, + options: &CompletionOptions, + ) -> Vec { + let mut matcher = NuMatcher::<()>::new(surround_remove(prefix.as_ref()), options); + let mut results = Vec::new(); + let span = reedline::Span { + start: span.start - offset, + end: span.end - offset, + }; + // TODO: use matcher.add_lazy to lazy evaluate an item if it matches the prefix + let mut add_suggestion = |value: String, + description: Option, + extra: Option>, + kind: SuggestionKind| { + results.push(SemanticSuggestion { + suggestion: Suggestion { + value, + span, + description, + extra, + ..Suggestion::default() + }, + kind: Some(kind), + }); + }; + + let working_set = self.temp_working_set.as_ref().unwrap_or(working_set); + let module = working_set.get_module(self.module_id); + + for (name, decl_id) in &module.decls { + let name = String::from_utf8_lossy(name).to_string(); + if matcher.matches(&name) { + let cmd = working_set.get_decl(*decl_id); + add_suggestion( + wrapped_name(name), + Some(cmd.description().to_string()), + None, + SuggestionKind::Command(cmd.command_type()), + ); + } + } + for (name, module_id) in &module.submodules { + let name = String::from_utf8_lossy(name).to_string(); + if matcher.matches(&name) { + let comments = working_set.get_module_comments(*module_id).map(|spans| { + spans + .iter() + .map(|sp| { + String::from_utf8_lossy(working_set.get_span_contents(*sp)).into() + }) + .collect::>() + }); + add_suggestion( + wrapped_name(name), + Some("Submodule".into()), + comments, + SuggestionKind::Module, + ); + } + } + for (name, var_id) in &module.constants { + let name = String::from_utf8_lossy(name).to_string(); + if matcher.matches(&name) { + let var = working_set.get_variable(*var_id); + add_suggestion( + wrapped_name(name), + var.const_val + .as_ref() + .and_then(|v| v.clone().coerce_into_string().ok()), + None, + SuggestionKind::Variable, + ); + } + } + results + } +} diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs index 5c2c542422..387df1b2d9 100644 --- a/crates/nu-cli/src/completions/flag_completions.rs +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -1,12 +1,12 @@ -use crate::completions::{completion_options::NuMatcher, Completer, CompletionOptions}; +use crate::completions::{ + completion_options::NuMatcher, Completer, CompletionOptions, SemanticSuggestion, SuggestionKind, +}; use nu_protocol::{ engine::{Stack, StateWorkingSet}, DeclId, Span, }; use reedline::Suggestion; -use super::{SemanticSuggestion, SuggestionKind}; - #[derive(Clone)] pub struct FlagCompletion { pub decl_id: DeclId, diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 5f16338bc2..b67d7db354 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -8,6 +8,7 @@ mod completion_options; mod custom_completions; mod directory_completions; mod dotnu_completions; +mod exportable_completions; mod file_completions; mod flag_completions; mod operator_completions; @@ -22,6 +23,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm}; pub use custom_completions::CustomCompletion; pub use directory_completions::DirectoryCompletion; pub use dotnu_completions::DotNuCompletion; +pub use exportable_completions::ExportableCompletion; pub use file_completions::{file_path_completion, FileCompletion}; pub use flag_completions::FlagCompletion; pub use operator_completions::OperatorCompletion; diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index e2b481f676..7559387999 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -11,6 +11,7 @@ use nu_engine::eval_block; use nu_parser::parse; use nu_path::expand_tilde; use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData}; +use nu_std::load_standard_library; use reedline::{Completer, Suggestion}; use rstest::{fixture, rstest}; use support::{ @@ -513,7 +514,7 @@ fn dotnu_completions() { match_suggestions(&vec!["sub.nu`"], &suggestions); - let expected = vec![ + let mut expected = vec![ "asdf.nu", "bar.nu", "bat.nu", @@ -546,6 +547,8 @@ fn dotnu_completions() { match_suggestions(&expected, &suggestions); // Test use completion + expected.push("std"); + expected.push("std-rfc"); let completion_str = "use "; let suggestions = completer.complete(completion_str, completion_str.len()); @@ -577,6 +580,65 @@ fn dotnu_completions() { match_dir_content_for_dotnu(dir_content, &suggestions); } +#[test] +fn dotnu_stdlib_completions() { + let (_, _, mut engine, stack) = new_dotnu_engine(); + assert!(load_standard_library(&mut engine).is_ok()); + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + + let completion_str = "export use std/ass"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["assert"], &suggestions); + + let completion_str = "use `std-rfc/cli"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["clip"], &suggestions); + + let completion_str = "use \"std"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["\"std", "\"std-rfc"], &suggestions); + + let completion_str = "overlay use \'std-rfc/cli"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["clip"], &suggestions); +} + +#[test] +fn exportable_completions() { + let (_, _, mut engine, mut stack) = new_dotnu_engine(); + let code = r#"export module "🤔🐘" { + export const foo = "🤔🐘"; + }"#; + assert!(support::merge_input(code.as_bytes(), &mut engine, &mut stack).is_ok()); + assert!(load_standard_library(&mut engine).is_ok()); + + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + + let completion_str = "use std null"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["null-device", "null_device"], &suggestions); + + let completion_str = "export use std/assert eq"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["equal"], &suggestions); + + let completion_str = "use std/assert \"not eq"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["'not equal'"], &suggestions); + + let completion_str = "use std-rfc/clip ['prefi"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["prefix"], &suggestions); + + let completion_str = "use std/math [E, `TAU"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["TAU"], &suggestions); + + let completion_str = "use 🤔🐘 'foo"; + let suggestions = completer.complete(completion_str, completion_str.len()); + match_suggestions(&vec!["foo"], &suggestions); +} + #[test] fn dotnu_completions_const_nu_lib_dirs() { let (_, _, engine, stack) = new_dotnu_engine(); diff --git a/crates/nu-lsp/Cargo.toml b/crates/nu-lsp/Cargo.toml index 5f3fabbf19..fbc444b418 100644 --- a/crates/nu-lsp/Cargo.toml +++ b/crates/nu-lsp/Cargo.toml @@ -28,6 +28,7 @@ url = { workspace = true } nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" } nu-command = { path = "../nu-command", version = "0.103.1" } nu-engine = { path = "../nu-engine", version = "0.103.1" } +nu-std = { path = "../nu-std", version = "0.103.1" } nu-test-support = { path = "../nu-test-support", version = "0.103.1" } assert-json-diff = "2.0" diff --git a/crates/nu-lsp/src/completion.rs b/crates/nu-lsp/src/completion.rs index 16ae33567f..110f5d4f0f 100644 --- a/crates/nu-lsp/src/completion.rs +++ b/crates/nu-lsp/src/completion.rs @@ -28,22 +28,15 @@ impl LanguageServer { .and_then(|s| s.chars().next()) .is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c)); - let (results, engine_state) = if need_fallback { - let engine_state = Arc::new(self.initial_engine_state.clone()); - let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); - ( - completer.fetch_completions_at(&file_text[..location], location), - engine_state, - ) + self.need_parse |= need_fallback; + let engine_state = Arc::new(self.new_engine_state()); + let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); + let results = if need_fallback { + completer.fetch_completions_at(&file_text[..location], location) } else { - let engine_state = Arc::new(self.new_engine_state()); - let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new())); let file_path = uri_to_path(&path_uri); let filename = file_path.to_str()?; - ( - completer.fetch_completions_within_file(filename, location, &file_text), - engine_state, - ) + completer.fetch_completions_within_file(filename, location, &file_text) }; let docs = self.docs.lock().ok()?; @@ -63,10 +56,8 @@ impl LanguageServer { } let span = r.suggestion.span; - let range = span_to_range(&Span::new(span.start, span.end), file, 0); - let text_edit = Some(CompletionTextEdit::Edit(TextEdit { - range, + range: span_to_range(&Span::new(span.start, span.end), file, 0), new_text: label_value.clone(), })); @@ -236,7 +227,7 @@ mod tests { "detail": "Edit nu configurations.", "textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, }, "newText": "config nu " - } + }, }, ]) ); @@ -549,4 +540,96 @@ mod tests { ]) ); } + + #[test] + fn complete_use_arguments() { + let (client_connection, _recv) = initialize_language_server(None, None); + + let mut script = fixtures(); + script.push("lsp"); + script.push("completion"); + script.push("use.nu"); + let script = path_to_uri(&script); + + open_unchecked(&client_connection, script.clone()); + let resp = send_complete_request(&client_connection, script.clone(), 4, 17); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "std-rfc", + "labelDetails": { "description": "module" }, + "textEdit": { + "newText": "std-rfc", + "range": { "start": { "character": 11, "line": 4 }, "end": { "character": 17, "line": 4 } } + }, + "kind": 9 // module kind + } + ]) + ); + + let resp = send_complete_request(&client_connection, script.clone(), 5, 22); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "clip", + "labelDetails": { "description": "module" }, + "textEdit": { + "newText": "clip", + "range": { "start": { "character": 19, "line": 5 }, "end": { "character": 23, "line": 5 } } + }, + "kind": 9 // module kind + } + ]) + ); + + let resp = send_complete_request(&client_connection, script.clone(), 5, 35); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "paste", + "labelDetails": { "description": "custom" }, + "textEdit": { + "newText": "paste", + "range": { "start": { "character": 32, "line": 5 }, "end": { "character": 37, "line": 5 } } + }, + "kind": 2 + } + ]) + ); + + let resp = send_complete_request(&client_connection, script.clone(), 6, 14); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "null_device", + "labelDetails": { "description": "variable" }, + "textEdit": { + "newText": "null_device", + "range": { "start": { "character": 8, "line": 6 }, "end": { "character": 14, "line": 6 } } + }, + "kind": 6 // variable kind + } + ]) + ); + + let resp = send_complete_request(&client_connection, script, 7, 13); + assert_json_include!( + actual: result_from_message(resp), + expected: serde_json::json!([ + { + "label": "foo", + "labelDetails": { "description": "variable" }, + "textEdit": { + "newText": "foo", + "range": { "start": { "character": 11, "line": 7 }, "end": { "character": 14, "line": 7 } } + }, + "kind": 6 // variable kind + } + ]) + ); + } } diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 027c1ffe8c..d48e425c43 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -440,6 +440,7 @@ mod tests { TextDocumentPositionParams, WorkDoneProgressParams, }; use nu_protocol::{debugger::WithoutDebug, engine::Stack, PipelineData, ShellError, Value}; + use nu_std::load_standard_library; use std::sync::mpsc::{self, Receiver}; use std::time::Duration; @@ -455,6 +456,7 @@ mod tests { let engine_state = nu_cmd_lang::create_default_context(); let mut engine_state = nu_command::add_shell_command_context(engine_state); engine_state.generate_nu_constant(); + assert!(load_standard_library(&mut engine_state).is_ok()); let cwd = std::env::current_dir().expect("Could not get current working directory."); engine_state.add_env_var( "PWD".into(), diff --git a/tests/fixtures/lsp/completion/use.nu b/tests/fixtures/lsp/completion/use.nu new file mode 100644 index 0000000000..8439560fb5 --- /dev/null +++ b/tests/fixtures/lsp/completion/use.nu @@ -0,0 +1,8 @@ +export module "🤔🐘" { + export const foo = "🤔🐘"; +} + +export use std-rf +export use std-rfc/clip [ copy, paste ] +use std null_d +use 🤔🐘 [ foo, ]