mirror of
https://github.com/nushell/nushell.git
synced 2025-03-13 06:58:47 +01:00
feat(completion): stdlib virtual path completion & exportable completion
This commit is contained in:
parent
81e496673e
commit
63d9867b60
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3541,6 +3541,7 @@ dependencies = [
|
||||
"nu-path",
|
||||
"nu-plugin-engine",
|
||||
"nu-protocol",
|
||||
"nu-std",
|
||||
"nu-test-support",
|
||||
"nu-utils",
|
||||
"nucleo-matcher",
|
||||
|
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
107
crates/nu-cli/src/completions/exportable_completions.rs
Normal file
107
crates/nu-cli/src/completions/exportable_completions.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user