mirror of
https://github.com/nushell/nushell.git
synced 2025-04-28 07:08:20 +02:00
feat(completion): stdlib virtual path completion & exportable completion (#15270)
# Description More completions for `use` command. ~Also optimizes the span fix of #15238 to allow changing the text after the cursor.~ # User-Facing Changes <img width="299" alt="image" src="https://github.com/user-attachments/assets/a5c45f46-40e4-4c50-9408-7b147ed11dc4" /> <img width="383" alt="image" src="https://github.com/user-attachments/assets/fbeec173-511e-4c72-8995-bc1caa3ef0d3" /> # Tests + Formatting +3 # After Submitting
This commit is contained in:
parent
1dcaffb792
commit
6c0b65b570
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -3542,6 +3542,7 @@ dependencies = [
|
|||||||
"nu-path",
|
"nu-path",
|
||||||
"nu-plugin-engine",
|
"nu-plugin-engine",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
|
"nu-std",
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-utils",
|
"nu-utils",
|
||||||
"nucleo-matcher",
|
"nucleo-matcher",
|
||||||
@ -3827,6 +3828,7 @@ dependencies = [
|
|||||||
"nu-glob",
|
"nu-glob",
|
||||||
"nu-parser",
|
"nu-parser",
|
||||||
"nu-protocol",
|
"nu-protocol",
|
||||||
|
"nu-std",
|
||||||
"nu-test-support",
|
"nu-test-support",
|
||||||
"nu-utils",
|
"nu-utils",
|
||||||
"nucleo-matcher",
|
"nucleo-matcher",
|
||||||
|
@ -13,6 +13,7 @@ bench = false
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" }
|
||||||
nu-command = { path = "../nu-command", 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" }
|
nu-test-support = { path = "../nu-test-support", version = "0.103.1" }
|
||||||
rstest = { workspace = true, default-features = false }
|
rstest = { workspace = true, default-features = false }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
use crate::completions::{
|
use crate::completions::{
|
||||||
|
base::{SemanticSuggestion, SuggestionKind},
|
||||||
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
|
AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer,
|
||||||
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion,
|
CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion,
|
||||||
FlagCompletion, OperatorCompletion, VariableCompletion,
|
ExportableCompletion, FileCompletion, FlagCompletion, OperatorCompletion, VariableCompletion,
|
||||||
};
|
};
|
||||||
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
||||||
use nu_engine::eval_block;
|
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::{
|
use nu_protocol::{
|
||||||
ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse},
|
ast::{Argument, Block, Expr, Expression, FindMapResult, ListItem, Traverse},
|
||||||
debugger::WithoutDebug,
|
debugger::WithoutDebug,
|
||||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||||
PipelineData, Span, Type, Value,
|
PipelineData, Span, Type, Value,
|
||||||
};
|
};
|
||||||
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
use reedline::{Completer as ReedlineCompleter, Suggestion};
|
||||||
use std::{str, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::base::{SemanticSuggestion, SuggestionKind};
|
|
||||||
|
|
||||||
/// Used as the function `f` in find_map Traverse
|
/// Used as the function `f` in find_map Traverse
|
||||||
///
|
///
|
||||||
@ -57,8 +56,13 @@ fn find_pipeline_element_by_position<'a>(
|
|||||||
Expr::FullCellPath(fcp) => fcp
|
Expr::FullCellPath(fcp) => fcp
|
||||||
.head
|
.head
|
||||||
.find_map(working_set, &closure)
|
.find_map(working_set, &closure)
|
||||||
.or(Some(expr))
|
|
||||||
.map(FindMapResult::Found)
|
.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(),
|
.unwrap_or_default(),
|
||||||
Expr::Var(_) => FindMapResult::Found(expr),
|
Expr::Var(_) => FindMapResult::Found(expr),
|
||||||
Expr::AttributeBlock(ab) => ab
|
Expr::AttributeBlock(ab) => ab
|
||||||
@ -127,6 +131,18 @@ struct Context<'a> {
|
|||||||
offset: usize,
|
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<'_> {
|
impl Context<'_> {
|
||||||
fn new<'a>(
|
fn new<'a>(
|
||||||
working_set: &'a StateWorkingSet,
|
working_set: &'a StateWorkingSet,
|
||||||
@ -328,7 +344,8 @@ impl NuCompleter {
|
|||||||
// NOTE: the argument to complete is not necessarily the last one
|
// NOTE: the argument to complete is not necessarily the last one
|
||||||
// for lsp completion, we don't trim the text,
|
// for lsp completion, we don't trim the text,
|
||||||
// so that `def`s after pos can be completed
|
// 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();
|
let span = arg.span();
|
||||||
if span.contains(pos) {
|
if span.contains(pos) {
|
||||||
// if customized completion specified, it has highest priority
|
// if customized completion specified, it has highest priority
|
||||||
@ -379,9 +396,15 @@ impl NuCompleter {
|
|||||||
// complete according to expression type and command head
|
// complete according to expression type and command head
|
||||||
Argument::Positional(expr) => {
|
Argument::Positional(expr) => {
|
||||||
let command_head = working_set.get_span_contents(call.head);
|
let command_head = working_set.get_span_contents(call.head);
|
||||||
|
positional_arg_indices.push(arg_idx);
|
||||||
self.argument_completion_helper(
|
self.argument_completion_helper(
|
||||||
command_head,
|
PositionalArguments {
|
||||||
expr,
|
command_head,
|
||||||
|
positional_arg_indices,
|
||||||
|
arguments: &call.arguments,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
&ctx,
|
&ctx,
|
||||||
suggestions.is_empty(),
|
suggestions.is_empty(),
|
||||||
)
|
)
|
||||||
@ -389,6 +412,8 @@ impl NuCompleter {
|
|||||||
_ => vec![],
|
_ => vec![],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
} else if !matches!(arg, Argument::Named(_)) {
|
||||||
|
positional_arg_indices.push(arg_idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,18 +523,95 @@ impl NuCompleter {
|
|||||||
|
|
||||||
fn argument_completion_helper(
|
fn argument_completion_helper(
|
||||||
&self,
|
&self,
|
||||||
command_head: &[u8],
|
argument_info: PositionalArguments,
|
||||||
expr: &Expression,
|
pos: usize,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
need_fallback: bool,
|
need_fallback: bool,
|
||||||
) -> Vec<SemanticSuggestion> {
|
) -> Vec<SemanticSuggestion> {
|
||||||
|
let PositionalArguments {
|
||||||
|
command_head,
|
||||||
|
positional_arg_indices,
|
||||||
|
arguments,
|
||||||
|
expr,
|
||||||
|
} = argument_info;
|
||||||
// special commands
|
// special commands
|
||||||
match command_head {
|
match command_head {
|
||||||
// complete module file/directory
|
// 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
|
// should parse it to get modules/commands/consts to complete
|
||||||
b"use" | b"export use" | b"overlay use" | b"source-env" => {
|
b"use" | b"export use" => {
|
||||||
return self.process_completion(&mut DotNuCompletion, ctx);
|
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<SemanticSuggestion> {
|
||||||
|
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" => {
|
b"which" => {
|
||||||
let mut completer = CommandCompletion {
|
let mut completer = CommandCompletion {
|
||||||
|
@ -150,7 +150,7 @@ impl OriginalCwd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn surround_remove(partial: &str) -> String {
|
pub fn surround_remove(partial: &str) -> String {
|
||||||
for c in ['`', '"', '\''] {
|
for c in ['`', '"', '\''] {
|
||||||
if partial.starts_with(c) {
|
if partial.starts_with(c) {
|
||||||
let ret = partial.strip_prefix(c).unwrap_or(partial);
|
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_path::expand_tilde;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet, VirtualPath},
|
||||||
Span,
|
Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
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 {
|
||||||
|
/// e.g. use std/a<tab>
|
||||||
pub struct DotNuCompletion;
|
pub std_virtual_path: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Completer for DotNuCompletion {
|
impl Completer for DotNuCompletion {
|
||||||
fn fetch(
|
fn fetch(
|
||||||
@ -102,7 +107,7 @@ impl Completer for DotNuCompletion {
|
|||||||
|
|
||||||
// Fetch the files filtering the ones that ends with .nu
|
// Fetch the files filtering the ones that ends with .nu
|
||||||
// and transform them into suggestions
|
// and transform them into suggestions
|
||||||
let completions = file_path_completion(
|
let mut completions = file_path_completion(
|
||||||
span,
|
span,
|
||||||
partial,
|
partial,
|
||||||
&search_dirs
|
&search_dirs
|
||||||
@ -113,17 +118,60 @@ impl Completer for DotNuCompletion {
|
|||||||
working_set.permanent_state,
|
working_set.permanent_state,
|
||||||
stack,
|
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);
|
||||||
|
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
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// Different base dir, so we list the .nu files or folders
|
// Different base dir, so we list the .nu files or folders
|
||||||
.filter(|it| {
|
.filter(|it| {
|
||||||
// for paths with spaces in them
|
// for paths with spaces in them
|
||||||
let path = it.path.trim_end_matches('`');
|
let path = it.path.trim_end_matches('`');
|
||||||
path.ends_with(".nu") || path.ends_with(SEP)
|
path.ends_with(".nu") || it.is_dir
|
||||||
})
|
})
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
let append_whitespace =
|
let append_whitespace = !x.is_dir && (!start_with_backquote || end_with_backquote);
|
||||||
x.path.ends_with(".nu") && (!start_with_backquote || end_with_backquote);
|
|
||||||
// Re-calculate the span to replace
|
// Re-calculate the span to replace
|
||||||
let mut span_offset = 0;
|
let mut span_offset = 0;
|
||||||
let mut value = x.path.to_string();
|
let mut value = x.path.to_string();
|
||||||
|
111
crates/nu-cli/src/completions/exportable_completions.rs
Normal file
111
crates/nu-cli/src/completions/exportable_completions.rs
Normal file
@ -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<StateWorkingSet<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
};
|
||||||
|
// TODO: use matcher.add_lazy to lazy evaluate an item if it matches the prefix
|
||||||
|
let mut add_suggestion = |value: String,
|
||||||
|
description: Option<String>,
|
||||||
|
extra: Option<Vec<String>>,
|
||||||
|
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::<Vec<String>>()
|
||||||
|
});
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -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::{
|
use nu_protocol::{
|
||||||
engine::{Stack, StateWorkingSet},
|
engine::{Stack, StateWorkingSet},
|
||||||
DeclId, Span,
|
DeclId, Span,
|
||||||
};
|
};
|
||||||
use reedline::Suggestion;
|
use reedline::Suggestion;
|
||||||
|
|
||||||
use super::{SemanticSuggestion, SuggestionKind};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FlagCompletion {
|
pub struct FlagCompletion {
|
||||||
pub decl_id: DeclId,
|
pub decl_id: DeclId,
|
||||||
|
@ -8,6 +8,7 @@ mod completion_options;
|
|||||||
mod custom_completions;
|
mod custom_completions;
|
||||||
mod directory_completions;
|
mod directory_completions;
|
||||||
mod dotnu_completions;
|
mod dotnu_completions;
|
||||||
|
mod exportable_completions;
|
||||||
mod file_completions;
|
mod file_completions;
|
||||||
mod flag_completions;
|
mod flag_completions;
|
||||||
mod operator_completions;
|
mod operator_completions;
|
||||||
@ -22,6 +23,7 @@ pub use completion_options::{CompletionOptions, MatchAlgorithm};
|
|||||||
pub use custom_completions::CustomCompletion;
|
pub use custom_completions::CustomCompletion;
|
||||||
pub use directory_completions::DirectoryCompletion;
|
pub use directory_completions::DirectoryCompletion;
|
||||||
pub use dotnu_completions::DotNuCompletion;
|
pub use dotnu_completions::DotNuCompletion;
|
||||||
|
pub use exportable_completions::ExportableCompletion;
|
||||||
pub use file_completions::{file_path_completion, FileCompletion};
|
pub use file_completions::{file_path_completion, FileCompletion};
|
||||||
pub use flag_completions::FlagCompletion;
|
pub use flag_completions::FlagCompletion;
|
||||||
pub use operator_completions::OperatorCompletion;
|
pub use operator_completions::OperatorCompletion;
|
||||||
|
@ -11,6 +11,7 @@ use nu_engine::eval_block;
|
|||||||
use nu_parser::parse;
|
use nu_parser::parse;
|
||||||
use nu_path::expand_tilde;
|
use nu_path::expand_tilde;
|
||||||
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData};
|
use nu_protocol::{debugger::WithoutDebug, engine::StateWorkingSet, Config, PipelineData};
|
||||||
|
use nu_std::load_standard_library;
|
||||||
use reedline::{Completer, Suggestion};
|
use reedline::{Completer, Suggestion};
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use support::{
|
use support::{
|
||||||
@ -513,7 +514,7 @@ fn dotnu_completions() {
|
|||||||
|
|
||||||
match_suggestions(&vec!["sub.nu`"], &suggestions);
|
match_suggestions(&vec!["sub.nu`"], &suggestions);
|
||||||
|
|
||||||
let expected = vec![
|
let mut expected = vec![
|
||||||
"asdf.nu",
|
"asdf.nu",
|
||||||
"bar.nu",
|
"bar.nu",
|
||||||
"bat.nu",
|
"bat.nu",
|
||||||
@ -546,6 +547,8 @@ fn dotnu_completions() {
|
|||||||
match_suggestions(&expected, &suggestions);
|
match_suggestions(&expected, &suggestions);
|
||||||
|
|
||||||
// Test use completion
|
// Test use completion
|
||||||
|
expected.push("std");
|
||||||
|
expected.push("std-rfc");
|
||||||
let completion_str = "use ";
|
let completion_str = "use ";
|
||||||
let suggestions = completer.complete(completion_str, completion_str.len());
|
let suggestions = completer.complete(completion_str, completion_str.len());
|
||||||
|
|
||||||
@ -577,6 +580,65 @@ fn dotnu_completions() {
|
|||||||
match_dir_content_for_dotnu(dir_content, &suggestions);
|
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]
|
#[test]
|
||||||
fn dotnu_completions_const_nu_lib_dirs() {
|
fn dotnu_completions_const_nu_lib_dirs() {
|
||||||
let (_, _, engine, stack) = new_dotnu_engine();
|
let (_, _, engine, stack) = new_dotnu_engine();
|
||||||
|
@ -28,6 +28,7 @@ url = { workspace = true }
|
|||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" }
|
||||||
nu-command = { path = "../nu-command", version = "0.103.1" }
|
nu-command = { path = "../nu-command", version = "0.103.1" }
|
||||||
nu-engine = { path = "../nu-engine", 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" }
|
nu-test-support = { path = "../nu-test-support", version = "0.103.1" }
|
||||||
|
|
||||||
assert-json-diff = "2.0"
|
assert-json-diff = "2.0"
|
||||||
|
@ -28,22 +28,15 @@ impl LanguageServer {
|
|||||||
.and_then(|s| s.chars().next())
|
.and_then(|s| s.chars().next())
|
||||||
.is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c));
|
.is_some_and(|c| c.is_whitespace() || "|(){}[]<>,:;".contains(c));
|
||||||
|
|
||||||
let (results, engine_state) = if need_fallback {
|
self.need_parse |= need_fallback;
|
||||||
let engine_state = Arc::new(self.initial_engine_state.clone());
|
let engine_state = Arc::new(self.new_engine_state());
|
||||||
let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
|
let completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
|
||||||
(
|
let results = if need_fallback {
|
||||||
completer.fetch_completions_at(&file_text[..location], location),
|
completer.fetch_completions_at(&file_text[..location], location)
|
||||||
engine_state,
|
|
||||||
)
|
|
||||||
} else {
|
} 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 file_path = uri_to_path(&path_uri);
|
||||||
let filename = file_path.to_str()?;
|
let filename = file_path.to_str()?;
|
||||||
(
|
completer.fetch_completions_within_file(filename, location, &file_text)
|
||||||
completer.fetch_completions_within_file(filename, location, &file_text),
|
|
||||||
engine_state,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let docs = self.docs.lock().ok()?;
|
let docs = self.docs.lock().ok()?;
|
||||||
@ -63,10 +56,8 @@ impl LanguageServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let span = r.suggestion.span;
|
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 {
|
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(),
|
new_text: label_value.clone(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -236,7 +227,7 @@ mod tests {
|
|||||||
"detail": "Edit nu configurations.",
|
"detail": "Edit nu configurations.",
|
||||||
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
|
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
|
||||||
"newText": "config nu "
|
"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
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,6 +440,7 @@ mod tests {
|
|||||||
TextDocumentPositionParams, WorkDoneProgressParams,
|
TextDocumentPositionParams, WorkDoneProgressParams,
|
||||||
};
|
};
|
||||||
use nu_protocol::{debugger::WithoutDebug, engine::Stack, PipelineData, ShellError, Value};
|
use nu_protocol::{debugger::WithoutDebug, engine::Stack, PipelineData, ShellError, Value};
|
||||||
|
use nu_std::load_standard_library;
|
||||||
use std::sync::mpsc::{self, Receiver};
|
use std::sync::mpsc::{self, Receiver};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -455,6 +456,7 @@ mod tests {
|
|||||||
let engine_state = nu_cmd_lang::create_default_context();
|
let engine_state = nu_cmd_lang::create_default_context();
|
||||||
let mut engine_state = nu_command::add_shell_command_context(engine_state);
|
let mut engine_state = nu_command::add_shell_command_context(engine_state);
|
||||||
engine_state.generate_nu_constant();
|
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.");
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
||||||
engine_state.add_env_var(
|
engine_state.add_env_var(
|
||||||
"PWD".into(),
|
"PWD".into(),
|
||||||
|
8
tests/fixtures/lsp/completion/use.nu
vendored
Normal file
8
tests/fixtures/lsp/completion/use.nu
vendored
Normal file
@ -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, ]
|
Loading…
Reference in New Issue
Block a user