mirror of
https://github.com/nushell/nushell.git
synced 2025-03-13 15:08:43 +01:00
fix(lsp): completion of commands defined after the cursor (#15188)
# Description Completion feature in LSP can't deal with commands defined after the cursor before this PR. This PR adds an alternative completion route where text is not truncated and no extra `a` appended. This will also ease future implementation of [signature help](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_signatureHelp). # User-Facing Changes # Tests + Formatting +6 # After Submitting
This commit is contained in:
parent
93612974e0
commit
7555743ccc
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3814,6 +3814,7 @@ dependencies = [
|
||||
"nu-cli",
|
||||
"nu-cmd-lang",
|
||||
"nu-command",
|
||||
"nu-engine",
|
||||
"nu-glob",
|
||||
"nu-parser",
|
||||
"nu-protocol",
|
||||
|
@ -12,6 +12,19 @@ use super::completion_options::NuMatcher;
|
||||
|
||||
pub struct CellPathCompletion<'a> {
|
||||
pub full_cell_path: &'a FullCellPath,
|
||||
pub position: usize,
|
||||
}
|
||||
|
||||
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
|
||||
let (prefix_str, start) = match member {
|
||||
PathMember::String { val, span, .. } => (val.clone(), span.start),
|
||||
PathMember::Int { val, span, .. } => (val.to_string(), span.start),
|
||||
};
|
||||
let prefix_str = prefix_str
|
||||
.get(..pos + 1 - start)
|
||||
.map(str::to_string)
|
||||
.unwrap_or(prefix_str);
|
||||
(prefix_str, Span::new(start, pos + 1))
|
||||
}
|
||||
|
||||
impl Completer for CellPathCompletion<'_> {
|
||||
@ -24,24 +37,30 @@ impl Completer for CellPathCompletion<'_> {
|
||||
offset: usize,
|
||||
options: &CompletionOptions,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// empty tail is already handled as variable names completion
|
||||
let Some((prefix_member, path_members)) = self.full_cell_path.tail.split_last() else {
|
||||
return vec![];
|
||||
};
|
||||
let (mut prefix_str, span) = match prefix_member {
|
||||
PathMember::String { val, span, .. } => (val.clone(), span),
|
||||
PathMember::Int { val, span, .. } => (val.to_string(), span),
|
||||
};
|
||||
// strip the placeholder
|
||||
prefix_str.pop();
|
||||
let true_end = std::cmp::max(span.start, span.end - 1);
|
||||
let span = Span::new(span.start, true_end);
|
||||
let mut prefix_str = String::new();
|
||||
// position at dots, e.g. `$env.config.<TAB>`
|
||||
let mut span = Span::new(self.position + 1, self.position + 1);
|
||||
let mut path_member_num_before_pos = 0;
|
||||
for member in self.full_cell_path.tail.iter() {
|
||||
if member.span().end <= self.position {
|
||||
path_member_num_before_pos += 1;
|
||||
} else if member.span().contains(self.position) {
|
||||
(prefix_str, span) = prefix_from_path_member(member, self.position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let current_span = reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: true_end - offset,
|
||||
end: span.end - offset,
|
||||
};
|
||||
|
||||
let mut matcher = NuMatcher::new(prefix_str, options);
|
||||
let path_members = self
|
||||
.full_cell_path
|
||||
.tail
|
||||
.get(0..path_member_num_before_pos)
|
||||
.unwrap_or_default();
|
||||
let value = eval_cell_path(
|
||||
working_set,
|
||||
stack,
|
||||
|
@ -7,7 +7,7 @@ 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_protocol::{
|
||||
ast::{Argument, Expr, Expression, FindMapResult, Traverse},
|
||||
ast::{Argument, Block, Expr, Expression, FindMapResult, Traverse},
|
||||
debugger::WithoutDebug,
|
||||
engine::{Closure, EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, Span, Value,
|
||||
@ -21,7 +21,7 @@ use super::base::{SemanticSuggestion, SuggestionKind};
|
||||
///
|
||||
/// returns the inner-most pipeline_element of interest
|
||||
/// i.e. the one that contains given position and needs completion
|
||||
pub fn find_pipeline_element_by_position<'a>(
|
||||
fn find_pipeline_element_by_position<'a>(
|
||||
expr: &'a Expression,
|
||||
working_set: &'a StateWorkingSet,
|
||||
pos: usize,
|
||||
@ -76,9 +76,17 @@ pub fn find_pipeline_element_by_position<'a>(
|
||||
|
||||
/// Before completion, an additional character `a` is added to the source as a placeholder for correct parsing results.
|
||||
/// This function helps to strip it
|
||||
fn strip_placeholder<'a>(working_set: &'a StateWorkingSet, span: &Span) -> (Span, &'a [u8]) {
|
||||
let new_end = std::cmp::max(span.end - 1, span.start);
|
||||
let new_span = Span::new(span.start, new_end);
|
||||
fn strip_placeholder_if_any<'a>(
|
||||
working_set: &'a StateWorkingSet,
|
||||
span: &Span,
|
||||
strip: bool,
|
||||
) -> (Span, &'a [u8]) {
|
||||
let new_span = if strip {
|
||||
let new_end = std::cmp::max(span.end - 1, span.start);
|
||||
Span::new(span.start, new_end)
|
||||
} else {
|
||||
span.to_owned()
|
||||
};
|
||||
let prefix = working_set.get_span_contents(new_span);
|
||||
(new_span, prefix)
|
||||
}
|
||||
@ -90,6 +98,7 @@ fn strip_placeholder_with_rsplit<'a>(
|
||||
working_set: &'a StateWorkingSet,
|
||||
span: &Span,
|
||||
predicate: impl FnMut(&u8) -> bool,
|
||||
strip: bool,
|
||||
) -> (Span, &'a [u8]) {
|
||||
let span_content = working_set.get_span_contents(*span);
|
||||
let mut prefix = span_content
|
||||
@ -97,7 +106,7 @@ fn strip_placeholder_with_rsplit<'a>(
|
||||
.next()
|
||||
.unwrap_or(span_content);
|
||||
let start = span.end.saturating_sub(prefix.len());
|
||||
if !prefix.is_empty() {
|
||||
if strip && !prefix.is_empty() {
|
||||
prefix = &prefix[..prefix.len() - 1];
|
||||
}
|
||||
let end = start + prefix.len();
|
||||
@ -142,15 +151,11 @@ impl NuCompleter {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_completions_at(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
|
||||
pub fn fetch_completions_at(&self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
|
||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||
let offset = working_set.next_span_start();
|
||||
// TODO: Callers should be trimming the line themselves
|
||||
let line = if line.len() > pos { &line[..pos] } else { line };
|
||||
// Adjust offset so that the spans of the suggestions will start at the right
|
||||
// place even with `only_buffer_difference: true`
|
||||
let pos = offset + pos;
|
||||
|
||||
let block = parse(
|
||||
&mut working_set,
|
||||
Some("completer"),
|
||||
@ -158,19 +163,64 @@ impl NuCompleter {
|
||||
format!("{}a", line).as_bytes(),
|
||||
false,
|
||||
);
|
||||
let Some(element_expression) = block.find_map(&working_set, &|expr: &Expression| {
|
||||
find_pipeline_element_by_position(expr, &working_set, pos)
|
||||
self.fetch_completions_by_block(block, &working_set, pos, offset, line, true)
|
||||
}
|
||||
|
||||
/// For completion in LSP server.
|
||||
/// We don't truncate the contents in order
|
||||
/// to complete the definitions after the cursor.
|
||||
///
|
||||
/// And we avoid the placeholder to reuse the parsed blocks
|
||||
/// cached while handling other LSP requests, e.g. diagnostics
|
||||
pub fn fetch_completions_within_file(
|
||||
&self,
|
||||
filename: &str,
|
||||
pos: usize,
|
||||
contents: &str,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||
let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
|
||||
let Some(file_span) = working_set.get_span_for_filename(filename) else {
|
||||
return vec![];
|
||||
};
|
||||
let offset = file_span.start;
|
||||
self.fetch_completions_by_block(block.clone(), &working_set, pos, offset, contents, false)
|
||||
}
|
||||
|
||||
fn fetch_completions_by_block(
|
||||
&self,
|
||||
block: Arc<Block>,
|
||||
working_set: &StateWorkingSet,
|
||||
pos: usize,
|
||||
offset: usize,
|
||||
contents: &str,
|
||||
extra_placeholder: bool,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
// Adjust offset so that the spans of the suggestions will start at the right
|
||||
// place even with `only_buffer_difference: true`
|
||||
let mut pos_to_search = pos + offset;
|
||||
if !extra_placeholder {
|
||||
pos_to_search = pos_to_search.saturating_sub(1);
|
||||
}
|
||||
let Some(element_expression) = block.find_map(working_set, &|expr: &Expression| {
|
||||
find_pipeline_element_by_position(expr, working_set, pos_to_search)
|
||||
}) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
// line of element_expression
|
||||
// text of element_expression
|
||||
let start_offset = element_expression.span.start - offset;
|
||||
if let Some(line) = line.get(start_offset..) {
|
||||
self.complete_by_expression(&working_set, element_expression, offset, pos, line)
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
let Some(text) = contents.get(start_offset..pos) else {
|
||||
return vec![];
|
||||
};
|
||||
self.complete_by_expression(
|
||||
working_set,
|
||||
element_expression,
|
||||
offset,
|
||||
pos_to_search,
|
||||
text,
|
||||
extra_placeholder,
|
||||
)
|
||||
}
|
||||
|
||||
/// Complete given the expression of interest
|
||||
@ -180,13 +230,15 @@ impl NuCompleter {
|
||||
/// * `offset` - start offset of current working_set span
|
||||
/// * `pos` - cursor position, should be > offset
|
||||
/// * `prefix_str` - all the text before the cursor, within the `element_expression`
|
||||
pub fn complete_by_expression(
|
||||
/// * `strip` - whether to strip the extra placeholder from a span
|
||||
fn complete_by_expression(
|
||||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
element_expression: &Expression,
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
prefix_str: &str,
|
||||
strip: bool,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let mut suggestions: Vec<SemanticSuggestion> = vec![];
|
||||
|
||||
@ -196,18 +248,24 @@ impl NuCompleter {
|
||||
working_set,
|
||||
element_expression.span,
|
||||
offset,
|
||||
strip,
|
||||
);
|
||||
}
|
||||
Expr::FullCellPath(full_cell_path) => {
|
||||
// e.g. `$e<tab>` parsed as FullCellPath
|
||||
if full_cell_path.tail.is_empty() {
|
||||
// but `$e.<tab>` without placeholder should be taken as cell_path
|
||||
if full_cell_path.tail.is_empty() && !prefix_str.ends_with('.') {
|
||||
return self.variable_names_completion_helper(
|
||||
working_set,
|
||||
element_expression.span,
|
||||
offset,
|
||||
strip,
|
||||
);
|
||||
} else {
|
||||
let mut cell_path_completer = CellPathCompletion { full_cell_path };
|
||||
let mut cell_path_completer = CellPathCompletion {
|
||||
full_cell_path,
|
||||
position: if strip { pos - 1 } else { pos },
|
||||
};
|
||||
let ctx = Context::new(working_set, Span::unknown(), &[], offset);
|
||||
return self.process_completion(&mut cell_path_completer, &ctx);
|
||||
}
|
||||
@ -217,7 +275,7 @@ impl NuCompleter {
|
||||
let mut operator_completions = OperatorCompletion {
|
||||
left_hand_side: lhs.as_ref(),
|
||||
};
|
||||
let (new_span, prefix) = strip_placeholder(working_set, &op.span);
|
||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &op.span, strip);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
let results = self.process_completion(&mut operator_completions, &ctx);
|
||||
if !results.is_empty() {
|
||||
@ -230,13 +288,13 @@ impl NuCompleter {
|
||||
let span = attr.expr.span;
|
||||
span.contains(pos).then_some(span)
|
||||
}) {
|
||||
let (new_span, prefix) = strip_placeholder(working_set, &span);
|
||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
return self.process_completion(&mut AttributeCompletion, &ctx);
|
||||
};
|
||||
let span = ab.item.span;
|
||||
if span.contains(pos) {
|
||||
let (new_span, prefix) = strip_placeholder(working_set, &span);
|
||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
return self.process_completion(&mut AttributableCompletion, &ctx);
|
||||
}
|
||||
@ -258,6 +316,7 @@ impl NuCompleter {
|
||||
offset,
|
||||
need_internals,
|
||||
need_externals,
|
||||
strip,
|
||||
))
|
||||
}
|
||||
_ => (),
|
||||
@ -276,11 +335,14 @@ impl NuCompleter {
|
||||
if let Some(decl_id) = arg.expr().and_then(|e| e.custom_completion) {
|
||||
// for `--foo <tab>` and `--foo=<tab>`, the arg span should be trimmed
|
||||
let (new_span, prefix) = if matches!(arg, Argument::Named(_)) {
|
||||
strip_placeholder_with_rsplit(working_set, &span, |b| {
|
||||
*b == b'=' || *b == b' '
|
||||
})
|
||||
strip_placeholder_with_rsplit(
|
||||
working_set,
|
||||
&span,
|
||||
|b| *b == b'=' || *b == b' ',
|
||||
strip,
|
||||
)
|
||||
} else {
|
||||
strip_placeholder(working_set, &span)
|
||||
strip_placeholder_if_any(working_set, &span, strip)
|
||||
};
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
|
||||
@ -296,18 +358,24 @@ impl NuCompleter {
|
||||
}
|
||||
|
||||
// normal arguments completion
|
||||
let (new_span, prefix) = strip_placeholder(working_set, &span);
|
||||
let (new_span, prefix) =
|
||||
strip_placeholder_if_any(working_set, &span, strip);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
let flag_completion_helper = || {
|
||||
let mut flag_completions = FlagCompletion {
|
||||
decl_id: call.decl_id,
|
||||
};
|
||||
self.process_completion(&mut flag_completions, &ctx)
|
||||
};
|
||||
suggestions.extend(match arg {
|
||||
// flags
|
||||
Argument::Named(_) | Argument::Unknown(_)
|
||||
if prefix.starts_with(b"-") =>
|
||||
{
|
||||
let mut flag_completions = FlagCompletion {
|
||||
decl_id: call.decl_id,
|
||||
};
|
||||
self.process_completion(&mut flag_completions, &ctx)
|
||||
flag_completion_helper()
|
||||
}
|
||||
// only when `strip` == false
|
||||
Argument::Positional(_) if prefix == b"-" => flag_completion_helper(),
|
||||
// complete according to expression type and command head
|
||||
Argument::Positional(expr) => {
|
||||
let command_head = working_set.get_span_contents(call.head);
|
||||
@ -339,6 +407,7 @@ impl NuCompleter {
|
||||
offset,
|
||||
true,
|
||||
true,
|
||||
strip,
|
||||
);
|
||||
// flags of sudo/doas can still be completed by external completer
|
||||
if !commands.is_empty() {
|
||||
@ -357,16 +426,17 @@ impl NuCompleter {
|
||||
String::from_utf8_lossy(bytes).to_string()
|
||||
})
|
||||
.collect();
|
||||
let mut new_span = span;
|
||||
// strip the placeholder
|
||||
if let Some(last) = text_spans.last_mut() {
|
||||
last.pop();
|
||||
if strip {
|
||||
if let Some(last) = text_spans.last_mut() {
|
||||
last.pop();
|
||||
new_span = Span::new(span.start, span.end.saturating_sub(1));
|
||||
}
|
||||
}
|
||||
if let Some(external_result) = self.external_completion(
|
||||
closure,
|
||||
&text_spans,
|
||||
offset,
|
||||
Span::new(span.start, span.end.saturating_sub(1)),
|
||||
) {
|
||||
if let Some(external_result) =
|
||||
self.external_completion(closure, &text_spans, offset, new_span)
|
||||
{
|
||||
suggestions.extend(external_result);
|
||||
return suggestions;
|
||||
}
|
||||
@ -380,10 +450,12 @@ impl NuCompleter {
|
||||
|
||||
// if no suggestions yet, fallback to file completion
|
||||
if suggestions.is_empty() {
|
||||
let (new_span, prefix) =
|
||||
strip_placeholder_with_rsplit(working_set, &element_expression.span, |c| {
|
||||
*c == b' '
|
||||
});
|
||||
let (new_span, prefix) = strip_placeholder_with_rsplit(
|
||||
working_set,
|
||||
&element_expression.span,
|
||||
|c| *c == b' ',
|
||||
strip,
|
||||
);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
suggestions.extend(self.process_completion(&mut FileCompletion, &ctx));
|
||||
}
|
||||
@ -395,8 +467,9 @@ impl NuCompleter {
|
||||
working_set: &StateWorkingSet,
|
||||
span: Span,
|
||||
offset: usize,
|
||||
strip: bool,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let (new_span, prefix) = strip_placeholder(working_set, &span);
|
||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||
if !prefix.starts_with(b"$") {
|
||||
return vec![];
|
||||
}
|
||||
@ -411,12 +484,13 @@ impl NuCompleter {
|
||||
offset: usize,
|
||||
internals: bool,
|
||||
externals: bool,
|
||||
strip: bool,
|
||||
) -> Vec<SemanticSuggestion> {
|
||||
let mut command_completions = CommandCompletion {
|
||||
internals,
|
||||
externals,
|
||||
};
|
||||
let (new_span, prefix) = strip_placeholder(working_set, &span);
|
||||
let (new_span, prefix) = strip_placeholder_if_any(working_set, &span, strip);
|
||||
let ctx = Context::new(working_set, new_span, prefix, offset);
|
||||
self.process_completion(&mut command_completions, &ctx)
|
||||
}
|
||||
@ -644,7 +718,7 @@ mod completer_tests {
|
||||
result.err().unwrap()
|
||||
);
|
||||
|
||||
let mut completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
|
||||
let completer = NuCompleter::new(engine_state.into(), Arc::new(Stack::new()));
|
||||
let dataset = [
|
||||
("1 bit-sh", true, "b", vec!["bit-shl", "bit-shr"]),
|
||||
("1.0 bit-sh", false, "b", vec![]),
|
||||
|
@ -26,6 +26,7 @@ url = { workspace = true }
|
||||
[dev-dependencies]
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.102.1" }
|
||||
nu-command = { path = "../nu-command", version = "0.102.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.102.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.102.1" }
|
||||
|
||||
assert-json-diff = "2.0"
|
||||
|
529
crates/nu-lsp/src/completion.rs
Normal file
529
crates/nu-lsp/src/completion.rs
Normal file
@ -0,0 +1,529 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{uri_to_path, LanguageServer};
|
||||
use lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
|
||||
CompletionResponse, CompletionTextEdit, Documentation, MarkupContent, MarkupKind, Range,
|
||||
TextEdit,
|
||||
};
|
||||
use nu_cli::{NuCompleter, SuggestionKind};
|
||||
use nu_protocol::engine::Stack;
|
||||
|
||||
impl LanguageServer {
|
||||
pub(crate) fn complete(&mut self, params: &CompletionParams) -> Option<CompletionResponse> {
|
||||
let path_uri = params.text_document_position.text_document.uri.to_owned();
|
||||
let docs = self.docs.lock().ok()?;
|
||||
let file = docs.get_document(&path_uri)?;
|
||||
let location = file.offset_at(params.text_document_position.position) as usize;
|
||||
let file_text = file.get_content(None).to_owned();
|
||||
drop(docs);
|
||||
// fallback to default completer where
|
||||
// the text is truncated to `location` and
|
||||
// an extra placeholder token is inserted for correct parsing
|
||||
let need_fallback = location == 0
|
||||
|| file_text
|
||||
.get(location - 1..location)
|
||||
.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,
|
||||
)
|
||||
} 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,
|
||||
)
|
||||
};
|
||||
|
||||
(!results.is_empty()).then_some(CompletionResponse::Array(
|
||||
results
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let mut start = params.text_document_position.position;
|
||||
start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32;
|
||||
let decl_id = r.kind.clone().and_then(|kind| {
|
||||
matches!(kind, SuggestionKind::Command(_))
|
||||
.then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?)
|
||||
});
|
||||
|
||||
CompletionItem {
|
||||
label: r.suggestion.value.clone(),
|
||||
label_details: r
|
||||
.kind
|
||||
.clone()
|
||||
.map(|kind| match kind {
|
||||
SuggestionKind::Type(t) => t.to_string(),
|
||||
SuggestionKind::Command(cmd) => cmd.to_string(),
|
||||
SuggestionKind::Module => "module".to_string(),
|
||||
SuggestionKind::Operator => "operator".to_string(),
|
||||
})
|
||||
.map(|s| CompletionItemLabelDetails {
|
||||
detail: None,
|
||||
description: Some(s),
|
||||
}),
|
||||
detail: r.suggestion.description,
|
||||
documentation: r
|
||||
.suggestion
|
||||
.extra
|
||||
.map(|ex| ex.join("\n"))
|
||||
.or(decl_id.map(|decl_id| {
|
||||
Self::get_decl_description(engine_state.get_decl(decl_id), true)
|
||||
}))
|
||||
.map(|value| {
|
||||
Documentation::MarkupContent(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value,
|
||||
})
|
||||
}),
|
||||
kind: Self::lsp_completion_item_kind(r.kind),
|
||||
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
|
||||
range: Range {
|
||||
start,
|
||||
end: params.text_document_position.position,
|
||||
},
|
||||
new_text: r.suggestion.value,
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn lsp_completion_item_kind(
|
||||
suggestion_kind: Option<SuggestionKind>,
|
||||
) -> Option<CompletionItemKind> {
|
||||
suggestion_kind.and_then(|suggestion_kind| match suggestion_kind {
|
||||
SuggestionKind::Type(t) => match t {
|
||||
nu_protocol::Type::String => Some(CompletionItemKind::VARIABLE),
|
||||
_ => None,
|
||||
},
|
||||
SuggestionKind::Command(c) => match c {
|
||||
nu_protocol::engine::CommandType::Keyword => Some(CompletionItemKind::KEYWORD),
|
||||
nu_protocol::engine::CommandType::Builtin => Some(CompletionItemKind::FUNCTION),
|
||||
nu_protocol::engine::CommandType::External => Some(CompletionItemKind::INTERFACE),
|
||||
_ => None,
|
||||
},
|
||||
SuggestionKind::Module => Some(CompletionItemKind::MODULE),
|
||||
SuggestionKind::Operator => Some(CompletionItemKind::OPERATOR),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::path_to_uri;
|
||||
use crate::tests::{initialize_language_server, open_unchecked, result_from_message};
|
||||
use assert_json_diff::assert_json_include;
|
||||
use lsp_server::{Connection, Message};
|
||||
use lsp_types::{
|
||||
request::{Completion, Request},
|
||||
CompletionParams, PartialResultParams, Position, TextDocumentIdentifier,
|
||||
TextDocumentPositionParams, Uri, WorkDoneProgressParams,
|
||||
};
|
||||
use nu_test_support::fs::fixtures;
|
||||
|
||||
fn send_complete_request(
|
||||
client_connection: &Connection,
|
||||
uri: Uri,
|
||||
line: u32,
|
||||
character: u32,
|
||||
) -> Message {
|
||||
client_connection
|
||||
.sender
|
||||
.send(Message::Request(lsp_server::Request {
|
||||
id: 2.into(),
|
||||
method: Completion::METHOD.to_string(),
|
||||
params: serde_json::to_value(CompletionParams {
|
||||
text_document_position: TextDocumentPositionParams {
|
||||
text_document: TextDocumentIdentifier { uri },
|
||||
position: Position { line, character },
|
||||
},
|
||||
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||
partial_result_params: PartialResultParams::default(),
|
||||
context: None,
|
||||
})
|
||||
.unwrap(),
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
client_connection
|
||||
.receiver
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_on_variable() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("var.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 2, 9);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "$greeting",
|
||||
"labelDetails": { "description": "string" },
|
||||
"textEdit": {
|
||||
"newText": "$greeting",
|
||||
"range": { "start": { "character": 5, "line": 2 }, "end": { "character": 9, "line": 2 } }
|
||||
},
|
||||
"kind": 6
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_command() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("command.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 0, 8);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
let detail_str = "detail";
|
||||
#[cfg(windows)]
|
||||
let detail_str = "detail\r";
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
// defined after the cursor
|
||||
{
|
||||
"label": "config n foo bar",
|
||||
"detail": detail_str,
|
||||
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
|
||||
"newText": "config n foo bar"
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "config nu",
|
||||
"detail": "Edit nu configurations.",
|
||||
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
|
||||
"newText": "config nu"
|
||||
},
|
||||
"kind": 3
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
// short flag
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 1, 18);
|
||||
assert!(result_from_message(resp).as_array().unwrap().contains(
|
||||
&serde_json::json!({
|
||||
"label": "-s",
|
||||
"detail": "test flag",
|
||||
"textEdit": { "range": { "start": { "line": 1, "character": 17 }, "end": { "line": 1, "character": 18 }, },
|
||||
"newText": "-s"
|
||||
},
|
||||
})
|
||||
));
|
||||
|
||||
// long flag
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 2, 22);
|
||||
assert!(result_from_message(resp).as_array().unwrap().contains(
|
||||
&serde_json::json!({
|
||||
"label": "--long",
|
||||
"detail": "test flag",
|
||||
"textEdit": { "range": { "start": { "line": 2, "character": 19 }, "end": { "line": 2, "character": 22 }, },
|
||||
"newText": "--long"
|
||||
},
|
||||
})
|
||||
));
|
||||
|
||||
// file path
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 2, 18);
|
||||
assert!(result_from_message(resp).as_array().unwrap().contains(
|
||||
&serde_json::json!({
|
||||
"label": "LICENSE",
|
||||
"textEdit": { "range": { "start": { "line": 2, "character": 17 }, "end": { "line": 2, "character": 18 }, },
|
||||
"newText": "LICENSE"
|
||||
},
|
||||
})
|
||||
));
|
||||
|
||||
// inside parenthesis
|
||||
let resp = send_complete_request(&client_connection, script, 10, 34);
|
||||
assert!(result_from_message(resp).as_array().unwrap().contains(
|
||||
&serde_json::json!({
|
||||
"label": "-g",
|
||||
"detail": "count indexes and split using grapheme clusters (all visible chars have length 1)",
|
||||
"textEdit": { "range": { "start": { "line": 10, "character": 33 }, "end": { "line": 10, "character": 34 }, },
|
||||
"newText": "-g"
|
||||
},
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_completion() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("fallback.nu");
|
||||
let script = path_to_uri(&script);
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
|
||||
// at the very beginning of a file
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 0, 0);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "alias",
|
||||
"labelDetails": { "description": "keyword" },
|
||||
"detail": "Alias a command (with optional flags) to a new name.",
|
||||
"textEdit": {
|
||||
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, },
|
||||
"newText": "alias"
|
||||
},
|
||||
"kind": 14
|
||||
}
|
||||
])
|
||||
);
|
||||
// after a white space character
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 3, 2);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "alias",
|
||||
"labelDetails": { "description": "keyword" },
|
||||
"detail": "Alias a command (with optional flags) to a new name.",
|
||||
"textEdit": {
|
||||
"range": { "start": { "line": 3, "character": 2 }, "end": { "line": 3, "character": 2 }, },
|
||||
"newText": "alias"
|
||||
},
|
||||
"kind": 14
|
||||
}
|
||||
])
|
||||
);
|
||||
// fallback file path completion
|
||||
let resp = send_complete_request(&client_connection, script, 5, 4);
|
||||
assert!(result_from_message(resp).as_array().unwrap().contains(
|
||||
&serde_json::json!({
|
||||
"label": "LICENSE",
|
||||
"textEdit": { "range": { "start": { "line": 5, "character": 3 }, "end": { "line": 5, "character": 4 }, },
|
||||
"newText": "LICENSE"
|
||||
},
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_command_with_line() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("utf_pipeline.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 0, 13);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "str trim",
|
||||
"labelDetails": { "description": "built-in" },
|
||||
"detail": "Trim whitespace or specific character.",
|
||||
"textEdit": {
|
||||
"range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, },
|
||||
"newText": "str trim"
|
||||
},
|
||||
"kind": 3
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_keyword() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("keyword.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 0, 2);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "overlay",
|
||||
"labelDetails": { "description": "keyword" },
|
||||
"textEdit": {
|
||||
"newText": "overlay",
|
||||
"range": { "start": { "character": 0, "line": 0 }, "end": { "character": 2, "line": 0 } }
|
||||
},
|
||||
"kind": 14
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_cell_path() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("cell_path.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 1, 5);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "1",
|
||||
"labelDetails": { "description": "record<1: string, 🤔🐘: table<baz: int>>" },
|
||||
"textEdit": {
|
||||
"newText": "1",
|
||||
"range": { "start": { "line": 1, "character": 5 }, "end": { "line": 1, "character": 5 } }
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
let resp = send_complete_request(&client_connection, script, 1, 10);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "baz",
|
||||
"labelDetails": { "description": "table<baz: int>" },
|
||||
"textEdit": {
|
||||
"newText": "baz",
|
||||
"range": { "start": { "line": 1, "character": 10 }, "end": { "line": 1, "character": 10 } }
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_with_external_completer() {
|
||||
let config = "$env.config.completions.external.completer = {|spans| ['--background']}";
|
||||
let (client_connection, _recv) = initialize_language_server(Some(config), None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("external.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 0, 11);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "--background",
|
||||
"labelDetails": { "description": "string" },
|
||||
"textEdit": {
|
||||
"newText": "--background",
|
||||
"range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 11 } }
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
// fallback completer, special argument treatment for `sudo`/`doas`
|
||||
let resp = send_complete_request(&client_connection, script, 0, 5);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "alias",
|
||||
"labelDetails": { "description": "keyword" },
|
||||
"detail": "Alias a command (with optional flags) to a new name.",
|
||||
"textEdit": {
|
||||
"range": { "start": { "line": 0, "character": 5 }, "end": { "line": 0, "character": 5 }, },
|
||||
"newText": "alias"
|
||||
},
|
||||
"kind": 14
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_operators() {
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("fallback.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
// fallback completer
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 7, 10);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "!=",
|
||||
"labelDetails": { "description": "operator" },
|
||||
"textEdit": {
|
||||
"newText": "!=",
|
||||
"range": { "start": { "character": 10, "line": 7 }, "end": { "character": 10, "line": 7 } }
|
||||
},
|
||||
"kind": 24 // operator kind
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
let resp = send_complete_request(&client_connection, script.clone(), 7, 15);
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "not-has",
|
||||
"labelDetails": { "description": "operator" },
|
||||
"textEdit": {
|
||||
"newText": "not-has",
|
||||
"range": { "start": { "character": 10, "line": 7 }, "end": { "character": 15, "line": 7 } }
|
||||
},
|
||||
"kind": 24 // operator kind
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
@ -56,7 +56,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn publish_diagnostics_variable_does_not_exists() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -87,7 +87,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn publish_diagnostics_fixed_unknown_variable() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -126,7 +126,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn publish_diagnostics_none() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
|
@ -1,9 +1,47 @@
|
||||
use crate::{Id, LanguageServer};
|
||||
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse};
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{path_to_uri, span_to_range, Id, LanguageServer};
|
||||
use lsp_textdocument::FullTextDocument;
|
||||
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Location};
|
||||
use nu_protocol::engine::{CachedFile, StateWorkingSet};
|
||||
use nu_protocol::Span;
|
||||
|
||||
impl LanguageServer {
|
||||
fn get_location_by_span<'a>(
|
||||
&self,
|
||||
files: impl Iterator<Item = &'a CachedFile>,
|
||||
span: &Span,
|
||||
) -> Option<Location> {
|
||||
for cached_file in files.into_iter() {
|
||||
if cached_file.covered_span.contains(span.start) {
|
||||
let path = Path::new(&*cached_file.name);
|
||||
if !path.is_file() {
|
||||
return None;
|
||||
}
|
||||
let target_uri = path_to_uri(path);
|
||||
if let Some(file) = self.docs.lock().ok()?.get_document(&target_uri) {
|
||||
return Some(Location {
|
||||
uri: target_uri,
|
||||
range: span_to_range(span, file, cached_file.covered_span.start),
|
||||
});
|
||||
} else {
|
||||
// in case where the document is not opened yet,
|
||||
// typically included by the `use/source` command
|
||||
let temp_doc = FullTextDocument::new(
|
||||
"nu".to_string(),
|
||||
0,
|
||||
String::from_utf8_lossy(cached_file.content.as_ref()).to_string(),
|
||||
);
|
||||
return Some(Location {
|
||||
uri: target_uri,
|
||||
range: span_to_range(span, &temp_doc, cached_file.covered_span.start),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn find_definition_span_by_id(
|
||||
working_set: &StateWorkingSet,
|
||||
id: &Id,
|
||||
@ -105,7 +143,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_for_none_existing_file() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut none_existent_path = root();
|
||||
none_existent_path.push("none-existent.nu");
|
||||
@ -117,7 +155,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_variable() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -142,7 +180,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_cell_path() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -167,7 +205,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_command() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -192,7 +230,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_command_unicode() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -217,7 +255,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_command_parameter() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -242,7 +280,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_variable_in_else_block() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -267,7 +305,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_variable_in_match_guard() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -292,7 +330,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_variable_in_each() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -317,7 +355,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_module() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -342,7 +380,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_module_in_another_file() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -367,7 +405,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_module_in_hide() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -392,7 +430,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn goto_definition_of_module_in_overlay() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
|
@ -160,7 +160,6 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: test for files loaded as user config
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::path_to_uri;
|
||||
@ -205,7 +204,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn inlay_hint_variable_type() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -232,7 +231,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn inlay_hint_assignment_type() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -260,7 +259,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn inlay_hint_parameter_names() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -319,4 +318,34 @@ mod tests {
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// https://github.com/nushell/nushell/pull/15071
|
||||
fn inlay_hint_for_nu_script_loaded_on_init() {
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("hints");
|
||||
script.push("type.nu");
|
||||
let script_path_str = script.to_str();
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
let config = format!("source {}", script_path_str.unwrap());
|
||||
let (client_connection, _recv) = initialize_language_server(Some(&config), None);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_inlay_hint_request(&client_connection, script.clone());
|
||||
|
||||
assert_json_eq!(
|
||||
result_from_message(resp),
|
||||
serde_json::json!([
|
||||
{ "position": { "line": 0, "character": 9 }, "label": ": int", "kind": 1 },
|
||||
{ "position": { "line": 1, "character": 7 }, "label": ": string", "kind": 1 },
|
||||
{ "position": { "line": 2, "character": 8 }, "label": ": bool", "kind": 1 },
|
||||
{ "position": { "line": 3, "character": 9 }, "label": ": float", "kind": 1 },
|
||||
{ "position": { "line": 4, "character": 8 }, "label": ": list", "kind": 1 },
|
||||
{ "position": { "line": 5, "character": 10 }, "label": ": record", "kind": 1 },
|
||||
{ "position": { "line": 6, "character": 11 }, "label": ": closure", "kind": 1 }
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,16 @@ use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
|
||||
use lsp_textdocument::{FullTextDocument, TextDocuments};
|
||||
use lsp_types::{
|
||||
request::{self, Request},
|
||||
CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionParams,
|
||||
CompletionResponse, CompletionTextEdit, Documentation, Hover, HoverContents, HoverParams,
|
||||
InlayHint, Location, MarkupContent, MarkupKind, OneOf, Position, Range, ReferencesOptions,
|
||||
RenameOptions, SemanticToken, SemanticTokenType, SemanticTokensLegend, SemanticTokensOptions,
|
||||
SemanticTokensServerCapabilities, ServerCapabilities, TextDocumentSyncKind, TextEdit, Uri,
|
||||
WorkDoneProgressOptions, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
|
||||
WorkspaceServerCapabilities,
|
||||
Hover, HoverContents, HoverParams, InlayHint, MarkupContent, MarkupKind, OneOf, Position,
|
||||
Range, ReferencesOptions, RenameOptions, SemanticToken, SemanticTokenType,
|
||||
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
|
||||
ServerCapabilities, TextDocumentSyncKind, Uri, WorkDoneProgressOptions, WorkspaceFolder,
|
||||
WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
|
||||
};
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use nu_cli::{NuCompleter, SuggestionKind};
|
||||
use nu_protocol::{
|
||||
ast::{Block, PathMember},
|
||||
engine::{CachedFile, Command, EngineState, Stack, StateDelta, StateWorkingSet},
|
||||
engine::{Command, EngineState, StateDelta, StateWorkingSet},
|
||||
DeclId, ModuleId, Span, Type, VarId,
|
||||
};
|
||||
use std::{
|
||||
@ -30,6 +27,7 @@ use symbols::SymbolCache;
|
||||
use workspace::{InternalMessage, RangePerDoc};
|
||||
|
||||
mod ast;
|
||||
mod completion;
|
||||
mod diagnostics;
|
||||
mod goto;
|
||||
mod hints;
|
||||
@ -329,6 +327,16 @@ impl LanguageServer {
|
||||
engine_state
|
||||
}
|
||||
|
||||
fn cache_parsed_block(&mut self, working_set: &mut StateWorkingSet, block: Arc<Block>) {
|
||||
if self.need_parse {
|
||||
// TODO: incremental parsing
|
||||
// add block to working_set for later references
|
||||
working_set.add_block(block.clone());
|
||||
self.cached_state_delta = Some(working_set.delta.clone());
|
||||
self.need_parse = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_and_find<'a>(
|
||||
&mut self,
|
||||
engine_state: &'a mut EngineState,
|
||||
@ -376,50 +384,11 @@ impl LanguageServer {
|
||||
self.semantic_tokens
|
||||
.insert(uri.clone(), file_semantic_tokens);
|
||||
}
|
||||
if self.need_parse {
|
||||
// TODO: incremental parsing
|
||||
// add block to working_set for later references
|
||||
working_set.add_block(block.clone());
|
||||
self.cached_state_delta = Some(working_set.delta.clone());
|
||||
self.need_parse = false;
|
||||
}
|
||||
drop(docs);
|
||||
self.cache_parsed_block(&mut working_set, block.clone());
|
||||
Some((block, span, working_set))
|
||||
}
|
||||
|
||||
fn get_location_by_span<'a>(
|
||||
&self,
|
||||
files: impl Iterator<Item = &'a CachedFile>,
|
||||
span: &Span,
|
||||
) -> Option<Location> {
|
||||
for cached_file in files.into_iter() {
|
||||
if cached_file.covered_span.contains(span.start) {
|
||||
let path = Path::new(&*cached_file.name);
|
||||
if !path.is_file() {
|
||||
return None;
|
||||
}
|
||||
let target_uri = path_to_uri(path);
|
||||
if let Some(file) = self.docs.lock().ok()?.get_document(&target_uri) {
|
||||
return Some(Location {
|
||||
uri: target_uri,
|
||||
range: span_to_range(span, file, cached_file.covered_span.start),
|
||||
});
|
||||
} else {
|
||||
// in case where the document is not opened yet, typically included by `nu -I`
|
||||
let temp_doc = FullTextDocument::new(
|
||||
"nu".to_string(),
|
||||
0,
|
||||
String::from_utf8_lossy(cached_file.content.as_ref()).to_string(),
|
||||
);
|
||||
return Some(Location {
|
||||
uri: target_uri,
|
||||
range: span_to_range(span, &temp_doc, cached_file.covered_span.start),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_lsp_request<P, H, R>(req: lsp_server::Request, mut param_handler: H) -> Response
|
||||
where
|
||||
P: serde::de::DeserializeOwned,
|
||||
@ -675,117 +644,48 @@ impl LanguageServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn complete(&mut self, params: &CompletionParams) -> Option<CompletionResponse> {
|
||||
let path_uri = params.text_document_position.text_document.uri.to_owned();
|
||||
let docs = self.docs.lock().ok()?;
|
||||
let file = docs.get_document(&path_uri)?;
|
||||
|
||||
let engine_state = Arc::new(self.initial_engine_state.clone());
|
||||
let mut completer = NuCompleter::new(engine_state.clone(), Arc::new(Stack::new()));
|
||||
|
||||
let location = file.offset_at(params.text_document_position.position) as usize;
|
||||
let results = completer.fetch_completions_at(&file.get_content(None)[..location], location);
|
||||
(!results.is_empty()).then_some(CompletionResponse::Array(
|
||||
results
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let mut start = params.text_document_position.position;
|
||||
start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32;
|
||||
let decl_id = r.kind.clone().and_then(|kind| {
|
||||
matches!(kind, SuggestionKind::Command(_))
|
||||
.then_some(engine_state.find_decl(r.suggestion.value.as_bytes(), &[])?)
|
||||
});
|
||||
|
||||
CompletionItem {
|
||||
label: r.suggestion.value.clone(),
|
||||
label_details: r
|
||||
.kind
|
||||
.clone()
|
||||
.map(|kind| match kind {
|
||||
SuggestionKind::Type(t) => t.to_string(),
|
||||
SuggestionKind::Command(cmd) => cmd.to_string(),
|
||||
SuggestionKind::Module => "module".to_string(),
|
||||
SuggestionKind::Operator => "operator".to_string(),
|
||||
})
|
||||
.map(|s| CompletionItemLabelDetails {
|
||||
detail: None,
|
||||
description: Some(s),
|
||||
}),
|
||||
detail: r.suggestion.description,
|
||||
documentation: r
|
||||
.suggestion
|
||||
.extra
|
||||
.map(|ex| ex.join("\n"))
|
||||
.or(decl_id.map(|decl_id| {
|
||||
Self::get_decl_description(engine_state.get_decl(decl_id), true)
|
||||
}))
|
||||
.map(|value| {
|
||||
Documentation::MarkupContent(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value,
|
||||
})
|
||||
}),
|
||||
kind: Self::lsp_completion_item_kind(r.kind),
|
||||
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
|
||||
range: Range {
|
||||
start,
|
||||
end: params.text_document_position.position,
|
||||
},
|
||||
new_text: r.suggestion.value,
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn lsp_completion_item_kind(
|
||||
suggestion_kind: Option<SuggestionKind>,
|
||||
) -> Option<CompletionItemKind> {
|
||||
suggestion_kind.and_then(|suggestion_kind| match suggestion_kind {
|
||||
SuggestionKind::Type(t) => match t {
|
||||
nu_protocol::Type::String => Some(CompletionItemKind::VARIABLE),
|
||||
_ => None,
|
||||
},
|
||||
SuggestionKind::Command(c) => match c {
|
||||
nu_protocol::engine::CommandType::Keyword => Some(CompletionItemKind::KEYWORD),
|
||||
nu_protocol::engine::CommandType::Builtin => Some(CompletionItemKind::FUNCTION),
|
||||
nu_protocol::engine::CommandType::External => Some(CompletionItemKind::INTERFACE),
|
||||
_ => None,
|
||||
},
|
||||
SuggestionKind::Operator => Some(CompletionItemKind::OPERATOR),
|
||||
SuggestionKind::Module => Some(CompletionItemKind::MODULE),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assert_json_diff::{assert_json_eq, assert_json_include};
|
||||
use assert_json_diff::assert_json_eq;
|
||||
use lsp_types::{
|
||||
notification::{
|
||||
DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification,
|
||||
},
|
||||
request::{Completion, HoverRequest, Initialize, Request, Shutdown},
|
||||
CompletionParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
||||
InitializedParams, PartialResultParams, Position, TextDocumentContentChangeEvent,
|
||||
TextDocumentIdentifier, TextDocumentItem, TextDocumentPositionParams,
|
||||
WorkDoneProgressParams,
|
||||
request::{HoverRequest, Initialize, Request, Shutdown},
|
||||
DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializedParams, Position,
|
||||
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem,
|
||||
TextDocumentPositionParams, WorkDoneProgressParams,
|
||||
};
|
||||
use nu_protocol::{debugger::WithoutDebug, engine::Stack, PipelineData, ShellError, Value};
|
||||
use nu_test_support::fs::fixtures;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::mpsc::{self, Receiver};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Initialize the language server for test purposes
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `nu_config_code`: Optional user defined `config.nu` that is loaded on start
|
||||
/// - `params`: Optional client side capability parameters
|
||||
pub(crate) fn initialize_language_server(
|
||||
nu_config_code: Option<&str>,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> (Connection, Receiver<Result<()>>) {
|
||||
use std::sync::mpsc;
|
||||
let (client_connection, server_connection) = Connection::memory();
|
||||
let engine_state = nu_cmd_lang::create_default_context();
|
||||
let 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();
|
||||
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
||||
engine_state.add_env_var(
|
||||
"PWD".into(),
|
||||
nu_protocol::Value::test_string(cwd.to_string_lossy()),
|
||||
);
|
||||
if let Some(code) = nu_config_code {
|
||||
assert!(merge_input(code.as_bytes(), &mut engine_state, &mut Stack::new()).is_ok());
|
||||
}
|
||||
|
||||
let (client_connection, server_connection) = Connection::memory();
|
||||
let lsp_server =
|
||||
LanguageServer::initialize_connection(server_connection, None, engine_state).unwrap();
|
||||
|
||||
@ -816,9 +716,40 @@ mod tests {
|
||||
(client_connection, recv)
|
||||
}
|
||||
|
||||
/// merge_input executes the given input into the engine
|
||||
/// and merges the state
|
||||
fn merge_input(
|
||||
input: &[u8],
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<(), ShellError> {
|
||||
let (block, delta) = {
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
|
||||
let block = nu_parser::parse(&mut working_set, None, input, false);
|
||||
|
||||
assert!(working_set.parse_errors.is_empty());
|
||||
|
||||
(block, working_set.render())
|
||||
};
|
||||
|
||||
engine_state.merge_delta(delta)?;
|
||||
|
||||
assert!(nu_engine::eval_block::<WithoutDebug>(
|
||||
engine_state,
|
||||
stack,
|
||||
&block,
|
||||
PipelineData::Value(Value::nothing(Span::unknown()), None),
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
// Merge environment into the permanent state
|
||||
engine_state.merge_env(stack)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_on_request() {
|
||||
let (client_connection, recv) = initialize_language_server(None);
|
||||
let (client_connection, recv) = initialize_language_server(None, None);
|
||||
|
||||
client_connection
|
||||
.sender
|
||||
@ -956,7 +887,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_variable() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -975,7 +906,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_cell_path() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -1009,7 +940,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_custom_command() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -1033,7 +964,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_external_command() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -1059,7 +990,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_str_join() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -1083,7 +1014,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_module() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -1104,150 +1035,4 @@ mod tests {
|
||||
"\"# module doc\""
|
||||
);
|
||||
}
|
||||
|
||||
fn send_complete_request(
|
||||
client_connection: &Connection,
|
||||
uri: Uri,
|
||||
line: u32,
|
||||
character: u32,
|
||||
) -> Message {
|
||||
client_connection
|
||||
.sender
|
||||
.send(Message::Request(lsp_server::Request {
|
||||
id: 2.into(),
|
||||
method: Completion::METHOD.to_string(),
|
||||
params: serde_json::to_value(CompletionParams {
|
||||
text_document_position: TextDocumentPositionParams {
|
||||
text_document: TextDocumentIdentifier { uri },
|
||||
position: Position { line, character },
|
||||
},
|
||||
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||
partial_result_params: PartialResultParams::default(),
|
||||
context: None,
|
||||
})
|
||||
.unwrap(),
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
client_connection
|
||||
.receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_on_variable() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("var.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 2, 9);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "$greeting",
|
||||
"labelDetails": { "description": "string" },
|
||||
"textEdit": {
|
||||
"newText": "$greeting",
|
||||
"range": { "start": { "character": 5, "line": 2 }, "end": { "character": 9, "line": 2 } }
|
||||
},
|
||||
"kind": 6
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_command_with_space() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("command.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 0, 8);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "config nu",
|
||||
"detail": "Edit nu configurations.",
|
||||
"textEdit": { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 8 }, },
|
||||
"newText": "config nu"
|
||||
},
|
||||
"kind": 3
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_command_with_line() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("utf_pipeline.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 0, 13);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "str trim",
|
||||
"labelDetails": { "description": "built-in" },
|
||||
"detail": "Trim whitespace or specific character.",
|
||||
"textEdit": {
|
||||
"range": { "start": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 13 }, },
|
||||
"newText": "str trim"
|
||||
},
|
||||
"kind": 3
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_keyword() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("completion");
|
||||
script.push("keyword.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
let resp = send_complete_request(&client_connection, script, 0, 2);
|
||||
|
||||
assert_json_include!(
|
||||
actual: result_from_message(resp),
|
||||
expected: serde_json::json!([
|
||||
{
|
||||
"label": "overlay",
|
||||
"labelDetails": { "description": "keyword" },
|
||||
"textEdit": {
|
||||
"newText": "overlay",
|
||||
"range": { "start": { "character": 0, "line": 0 }, "end": { "character": 2, "line": 0 } }
|
||||
},
|
||||
"kind": 14
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_correct_documentation_on_let() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -170,7 +170,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn hover_on_command_after_full_content_change() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -205,7 +205,7 @@ hello"#,
|
||||
|
||||
#[test]
|
||||
fn hover_on_command_after_partial_content_change() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -244,7 +244,7 @@ hello"#,
|
||||
|
||||
#[test]
|
||||
fn open_document_with_utf_char() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
|
@ -10,7 +10,7 @@ use nu_protocol::{
|
||||
|
||||
use crate::{span_to_range, LanguageServer};
|
||||
|
||||
/// Important for keep spans in increasing order,
|
||||
/// Important to keep spans in increasing order,
|
||||
/// since `SemanticToken`s are created by relative positions
|
||||
/// to one's previous token
|
||||
///
|
||||
@ -141,7 +141,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn semantic_token_internals() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
|
@ -352,7 +352,7 @@ mod tests {
|
||||
#[test]
|
||||
// for variable `$in/$it`, should not appear in symbols
|
||||
fn document_symbol_special_variables() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -368,7 +368,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn document_symbol_basic() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -410,7 +410,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn document_symbol_update() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -456,7 +456,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn workspace_symbol_current() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
@ -516,7 +516,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn workspace_symbol_other() {
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
|
@ -573,9 +573,10 @@ mod tests {
|
||||
let mut script = fixtures();
|
||||
script.push("lsp");
|
||||
script.push("workspace");
|
||||
let (client_connection, _recv) = initialize_language_server(Some(
|
||||
serde_json::json!({ "workspaceFolders": serde_json::Value::Null }),
|
||||
));
|
||||
let (client_connection, _recv) = initialize_language_server(
|
||||
None,
|
||||
Some(serde_json::json!({ "workspaceFolders": serde_json::Value::Null })),
|
||||
);
|
||||
script.push("foo.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
@ -588,6 +589,7 @@ mod tests {
|
||||
script.push("lsp");
|
||||
script.push("workspace");
|
||||
let (client_connection, _recv) = initialize_language_server(
|
||||
None,
|
||||
serde_json::to_value(InitializeParams {
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: path_to_uri(&script),
|
||||
@ -638,6 +640,7 @@ mod tests {
|
||||
script.push("lsp");
|
||||
script.push("workspace");
|
||||
let (client_connection, _recv) = initialize_language_server(
|
||||
None,
|
||||
serde_json::to_value(InitializeParams {
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: path_to_uri(&script),
|
||||
@ -688,6 +691,7 @@ mod tests {
|
||||
script.push("lsp");
|
||||
script.push("workspace");
|
||||
let (client_connection, _recv) = initialize_language_server(
|
||||
None,
|
||||
serde_json::to_value(InitializeParams {
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: path_to_uri(&script),
|
||||
@ -766,6 +770,7 @@ mod tests {
|
||||
script.push("lsp");
|
||||
script.push("workspace");
|
||||
let (client_connection, _recv) = initialize_language_server(
|
||||
None,
|
||||
serde_json::to_value(InitializeParams {
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: path_to_uri(&script),
|
||||
@ -834,6 +839,7 @@ mod tests {
|
||||
script.push("lsp");
|
||||
script.push("workspace");
|
||||
let (client_connection, _recv) = initialize_language_server(
|
||||
None,
|
||||
serde_json::to_value(InitializeParams {
|
||||
workspace_folders: Some(vec![WorkspaceFolder {
|
||||
uri: path_to_uri(&script),
|
||||
@ -927,7 +933,7 @@ mod tests {
|
||||
script.push("foo.nu");
|
||||
let script = path_to_uri(&script);
|
||||
|
||||
let (client_connection, _recv) = initialize_language_server(None);
|
||||
let (client_connection, _recv) = initialize_language_server(None, None);
|
||||
open_unchecked(&client_connection, script.clone());
|
||||
|
||||
let message = send_document_highlight_request(&client_connection, script.clone(), 3, 5);
|
||||
|
2
tests/fixtures/lsp/completion/cell_path.nu
vendored
Normal file
2
tests/fixtures/lsp/completion/cell_path.nu
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
const foo = {"1": "bar" "🤔🐘": [{"baz": 1}]}
|
||||
$foo.🤔🐘.0.b
|
11
tests/fixtures/lsp/completion/command.nu
vendored
11
tests/fixtures/lsp/completion/command.nu
vendored
@ -1 +1,12 @@
|
||||
config n
|
||||
config n foo bar -
|
||||
config n foo bar l --l
|
||||
|
||||
# detail
|
||||
def "config n foo bar" [
|
||||
f: path
|
||||
--long (-s): int # test flag
|
||||
] {
|
||||
echo "🤔🐘"
|
||||
| str substring (str substring -)
|
||||
}
|
||||
|
1
tests/fixtures/lsp/completion/external.nu
vendored
Normal file
1
tests/fixtures/lsp/completion/external.nu
vendored
Normal file
@ -0,0 +1 @@
|
||||
sudo --back
|
8
tests/fixtures/lsp/completion/fallback.nu
vendored
Normal file
8
tests/fixtures/lsp/completion/fallback.nu
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
let greeting = "Hello"
|
||||
|
||||
echo $gre
|
||||
| st
|
||||
|
||||
ls l
|
||||
|
||||
$greeting not-h
|
Loading…
Reference in New Issue
Block a user