mirror of
https://github.com/nushell/nushell.git
synced 2025-04-27 22:58: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-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",
|
||||
|
@ -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 }
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<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,
|
||||
};
|
||||
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" => {
|
||||
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 ['`', '"', '\''] {
|
||||
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,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::<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
|
||||
.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();
|
||||
|
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::{
|
||||
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, 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();
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
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