feat(completion): stdlib virtual path completion & exportable completion

This commit is contained in:
blindfs 2025-03-07 15:38:42 +08:00
parent 81e496673e
commit 63d9867b60
9 changed files with 317 additions and 31 deletions

1
Cargo.lock generated
View File

@ -3541,6 +3541,7 @@ dependencies = [
"nu-path",
"nu-plugin-engine",
"nu-protocol",
"nu-std",
"nu-test-support",
"nu-utils",
"nucleo-matcher",

View File

@ -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 }

View File

@ -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 [<tab>
.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<usize>,
/// 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<SemanticSuggestion> {
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<SemanticSuggestion> {
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 {

View File

@ -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);

View File

@ -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<tab>
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::<String>();
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();

View File

@ -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<StateWorkingSet<'a>>,
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<str>,
span: Span,
offset: usize,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
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<String>,
extra: Option<Vec<String>>,
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::<Vec<String>>()
});
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
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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();