diff --git a/Cargo.lock b/Cargo.lock index 8358295a59..5af250297e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3541,6 +3541,7 @@ dependencies = [ "nu-path", "nu-plugin-engine", "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 039ebdd398..bbb2860f7a 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.102.1" } nu-command = { path = "../nu-command", version = "0.102.1" } +nu-std = { path = "../nu-std", version = "0.102.1" } nu-test-support = { path = "../nu-test-support", version = "0.102.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 ec0c10effe..2470fd0792 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); } } } @@ -497,18 +522,97 @@ 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, + in_list: false, + }; + let mut complete_on_list_items = |items: &[ListItem]| -> Vec { + exportable_completion.in_list = true; + 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).min(pos + 1)), + 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 f5adb6aecd..1013f08638 100644 --- a/crates/nu-cli/src/completions/completion_common.rs +++ b/crates/nu-cli/src/completions/completion_common.rs @@ -139,7 +139,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..1851060972 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,57 @@ 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); + if matcher.matches(&path) { + completions.push(FileSuggestion { + path, + span, + 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(); + if matcher.matches(&path) { + completions.push(FileSuggestion { + path, + span, + style: None, + is_dir: matches!(sub_vp, VirtualPath::Dir(_)), + }); + } + } + } + } + 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..57cdc2add0 --- /dev/null +++ b/crates/nu-cli/src/completions/exportable_completions.rs @@ -0,0 +1,107 @@ +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>, + pub in_list: bool, +} + +/// 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, + }; + let mut add_suggestion = |value: String, + description: Option, + extra: Option>, + kind: SuggestionKind| { + results.push(SemanticSuggestion { + suggestion: Suggestion { + value, + span, + description, + extra, + append_whitespace: !self.in_list, + ..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), None, 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 f0c9fcaef0..2fb409239e 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, PipelineData}; +use nu_std::load_standard_library; use reedline::{Completer, Suggestion}; use rstest::{fixture, rstest}; use support::{ @@ -468,7 +469,7 @@ fn dotnu_completions() { match_suggestions(&vec!["sub.nu`"], &suggestions); - let expected = vec![ + let mut expected = vec![ "asdf.nu", "bar.nu", "bat.nu", @@ -501,6 +502,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()); @@ -532,6 +535,29 @@ 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 dotnu_completions_const_nu_lib_dirs() { let (_, _, engine, stack) = new_dotnu_engine();