Send LSP Completion Item Kind (#11443)

# Description

This commit fills in the completion item kind into the
`textDocument/completion` response so that LSP client can present more
information to the user.

It is an improvement in the context of #10794

# User-Facing Changes

Improved information display in editor's intelli-sense menu


![output](https://github.com/nushell/nushell/assets/16558417/991dc0a9-45d1-4718-8f22-29002d687b93)
This commit is contained in:
Marc Schreiber 2024-03-25 02:14:12 +01:00 committed by GitHub
parent d1a8992590
commit e7bdd08a04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 384 additions and 199 deletions

View File

@ -13,13 +13,13 @@ pub trait Completer {
offset: usize,
pos: usize,
options: &CompletionOptions,
) -> Vec<Suggestion>;
) -> Vec<SemanticSuggestion>;
fn get_sort_by(&self) -> SortBy {
SortBy::Ascending
}
fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
let mut filtered_items = items;
@ -27,13 +27,13 @@ pub trait Completer {
match self.get_sort_by() {
SortBy::LevenshteinDistance => {
filtered_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.value);
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value);
a_distance.cmp(&b_distance)
});
}
SortBy::Ascending => {
filtered_items.sort_by(|a, b| a.value.cmp(&b.value));
filtered_items.sort_by(|a, b| a.suggestion.value.cmp(&b.suggestion.value));
}
SortBy::None => {}
};
@ -41,3 +41,25 @@ pub trait Completer {
filtered_items
}
}
#[derive(Debug, Default, PartialEq)]
pub struct SemanticSuggestion {
pub suggestion: Suggestion,
pub kind: Option<SuggestionKind>,
}
// TODO: think about name: maybe suggestion context?
#[derive(Clone, Debug, PartialEq)]
pub enum SuggestionKind {
Command(nu_protocol::engine::CommandType),
Type(nu_protocol::Type),
}
impl From<Suggestion> for SemanticSuggestion {
fn from(suggestion: Suggestion) -> Self {
Self {
suggestion,
..Default::default()
}
}
}

View File

@ -1,4 +1,7 @@
use crate::completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy};
use crate::{
completions::{Completer, CompletionOptions, MatchAlgorithm, SortBy},
SuggestionKind,
};
use nu_parser::FlatShape;
use nu_protocol::{
engine::{CachedFile, EngineState, StateWorkingSet},
@ -7,6 +10,8 @@ use nu_protocol::{
use reedline::Suggestion;
use std::sync::Arc;
use super::SemanticSuggestion;
pub struct CommandCompletion {
engine_state: Arc<EngineState>,
flattened: Vec<(Span, FlatShape)>,
@ -83,7 +88,7 @@ impl CommandCompletion {
offset: usize,
find_externals: bool,
match_algorithm: MatchAlgorithm,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let partial = working_set.get_span_contents(span);
let filter_predicate = |command: &[u8]| match_algorithm.matches_u8(command, partial);
@ -91,13 +96,16 @@ impl CommandCompletion {
let mut results = working_set
.find_commands_by_predicate(filter_predicate, true)
.into_iter()
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
},
kind: Some(SuggestionKind::Command(x.2)),
})
.collect::<Vec<_>>();
@ -108,27 +116,34 @@ impl CommandCompletion {
let results_external = self
.external_command_completion(&partial, match_algorithm)
.into_iter()
.map(move |x| Suggestion {
value: x,
description: None,
style: None,
extra: None,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
});
let results_strings: Vec<String> =
results.clone().into_iter().map(|x| x.value).collect();
for external in results_external {
if results_strings.contains(&external.value) {
results.push(Suggestion {
value: format!("^{}", external.value),
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x,
description: None,
style: None,
extra: None,
span: external.span,
span: reedline::Span::new(span.start - offset, span.end - offset),
append_whitespace: true,
},
// TODO: is there a way to create a test?
kind: None,
});
let results_strings: Vec<String> =
results.iter().map(|x| x.suggestion.value.clone()).collect();
for external in results_external {
if results_strings.contains(&external.suggestion.value) {
results.push(SemanticSuggestion {
suggestion: Suggestion {
value: format!("^{}", external.suggestion.value),
description: None,
style: None,
extra: None,
span: external.suggestion.span,
append_whitespace: true,
},
kind: external.kind,
})
} else {
results.push(external)
@ -151,7 +166,7 @@ impl Completer for CommandCompletion {
offset: usize,
pos: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let last = self
.flattened
.iter()

View File

@ -14,6 +14,8 @@ use reedline::{Completer as ReedlineCompleter, Suggestion};
use std::str;
use std::sync::Arc;
use super::base::{SemanticSuggestion, SuggestionKind};
#[derive(Clone)]
pub struct NuCompleter {
engine_state: Arc<EngineState>,
@ -28,6 +30,10 @@ impl NuCompleter {
}
}
pub fn fetch_completions_at(&mut self, line: &str, pos: usize) -> Vec<SemanticSuggestion> {
self.completion_helper(line, pos)
}
// Process the completion for a given completer
fn process_completion<T: Completer>(
&self,
@ -37,7 +43,7 @@ impl NuCompleter {
new_span: Span,
offset: usize,
pos: usize,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let config = self.engine_state.get_config();
let options = CompletionOptions {
@ -62,7 +68,7 @@ impl NuCompleter {
spans: &[String],
offset: usize,
span: Span,
) -> Option<Vec<Suggestion>> {
) -> Option<Vec<SemanticSuggestion>> {
let block = self.engine_state.get_block(block_id);
let mut callee_stack = self
.stack
@ -107,7 +113,7 @@ impl NuCompleter {
None
}
fn completion_helper(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
fn completion_helper(&mut 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
@ -397,6 +403,9 @@ impl NuCompleter {
impl ReedlineCompleter for NuCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
self.completion_helper(line, pos)
.into_iter()
.map(|s| s.suggestion)
.collect()
}
}
@ -454,20 +463,23 @@ pub fn map_value_completions<'a>(
list: impl Iterator<Item = &'a Value>,
span: Span,
offset: usize,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
list.filter_map(move |x| {
// Match for string values
if let Ok(s) = x.coerce_string() {
return Some(Suggestion {
value: s,
description: None,
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
return Some(SemanticSuggestion {
suggestion: Suggestion {
value: s,
description: None,
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: false,
},
append_whitespace: false,
kind: Some(SuggestionKind::Type(x.get_type())),
});
}
@ -516,7 +528,10 @@ pub fn map_value_completions<'a>(
}
});
return Some(suggestion);
return Some(SemanticSuggestion {
suggestion,
kind: Some(SuggestionKind::Type(x.get_type())),
});
}
None
@ -568,13 +583,13 @@ mod completer_tests {
// Test whether the result begins with the expected value
result
.iter()
.for_each(|x| assert!(x.value.starts_with(begins_with)));
.for_each(|x| assert!(x.suggestion.value.starts_with(begins_with)));
// Test whether the result contains all the expected values
assert_eq!(
result
.iter()
.map(|x| expected_values.contains(&x.value.as_str()))
.map(|x| expected_values.contains(&x.suggestion.value.as_str()))
.filter(|x| *x)
.count(),
expected_values.len(),

View File

@ -7,10 +7,10 @@ use nu_protocol::{
PipelineData, Span, Type, Value,
};
use nu_utils::IgnoreCaseExt;
use reedline::Suggestion;
use std::collections::HashMap;
use std::sync::Arc;
use super::base::SemanticSuggestion;
use super::completer::map_value_completions;
pub struct CustomCompletion {
@ -42,7 +42,7 @@ impl Completer for CustomCompletion {
offset: usize,
pos: usize,
completion_options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
// Line position
let line_pos = pos - offset;
@ -145,15 +145,22 @@ impl Completer for CustomCompletion {
}
}
fn filter(prefix: &[u8], items: Vec<Suggestion>, options: &CompletionOptions) -> Vec<Suggestion> {
fn filter(
prefix: &[u8],
items: Vec<SemanticSuggestion>,
options: &CompletionOptions,
) -> Vec<SemanticSuggestion> {
items
.into_iter()
.filter(|it| match options.match_algorithm {
MatchAlgorithm::Prefix => match (options.case_sensitive, options.positional) {
(true, true) => it.value.as_bytes().starts_with(prefix),
(true, false) => it.value.contains(std::str::from_utf8(prefix).unwrap_or("")),
(true, true) => it.suggestion.value.as_bytes().starts_with(prefix),
(true, false) => it
.suggestion
.value
.contains(std::str::from_utf8(prefix).unwrap_or("")),
(false, positional) => {
let value = it.value.to_folded_case();
let value = it.suggestion.value.to_folded_case();
let prefix = std::str::from_utf8(prefix).unwrap_or("").to_folded_case();
if positional {
value.starts_with(&prefix)
@ -164,7 +171,7 @@ fn filter(prefix: &[u8], items: Vec<Suggestion>, options: &CompletionOptions) ->
},
MatchAlgorithm::Fuzzy => options
.match_algorithm
.matches_u8(it.value.as_bytes(), prefix),
.matches_u8(it.suggestion.value.as_bytes(), prefix),
})
.collect()
}

View File

@ -11,6 +11,8 @@ use reedline::Suggestion;
use std::path::{Path, MAIN_SEPARATOR as SEP};
use std::sync::Arc;
use super::SemanticSuggestion;
#[derive(Clone)]
pub struct DirectoryCompletion {
engine_state: Arc<EngineState>,
@ -35,7 +37,7 @@ impl Completer for DirectoryCompletion {
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let AdjustView { prefix, span, .. } = adjust_if_intermediate(&prefix, working_set, span);
// Filter only the folders
@ -48,16 +50,20 @@ impl Completer for DirectoryCompletion {
&self.stack,
)
.into_iter()
.map(move |x| Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: false,
},
append_whitespace: false,
// TODO????
kind: None,
})
.collect();
@ -65,7 +71,7 @@ impl Completer for DirectoryCompletion {
}
// Sort results prioritizing the non hidden folders
fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items
@ -75,15 +81,16 @@ impl Completer for DirectoryCompletion {
SortBy::Ascending => {
sorted_items.sort_by(|a, b| {
// Ignore trailing slashes in folder names when sorting
a.value
a.suggestion
.value
.trim_end_matches(SEP)
.cmp(b.value.trim_end_matches(SEP))
.cmp(b.suggestion.value.trim_end_matches(SEP))
});
}
SortBy::LevenshteinDistance => {
sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.value);
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value);
a_distance.cmp(&b_distance)
});
}
@ -91,11 +98,11 @@ impl Completer for DirectoryCompletion {
}
// Separate the results between hidden and non hidden
let mut hidden: Vec<Suggestion> = vec![];
let mut non_hidden: Vec<Suggestion> = vec![];
let mut hidden: Vec<SemanticSuggestion> = vec![];
let mut non_hidden: Vec<SemanticSuggestion> = vec![];
for item in sorted_items.into_iter() {
let item_path = Path::new(&item.value);
let item_path = Path::new(&item.suggestion.value);
if let Some(value) = item_path.file_name() {
if let Some(value) = value.to_str() {

View File

@ -9,6 +9,8 @@ use std::{
sync::Arc,
};
use super::SemanticSuggestion;
#[derive(Clone)]
pub struct DotNuCompletion {
engine_state: Arc<EngineState>,
@ -33,7 +35,7 @@ impl Completer for DotNuCompletion {
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).replace('`', "");
let mut search_dirs: Vec<String> = vec![];
@ -93,7 +95,7 @@ impl Completer for DotNuCompletion {
// Fetch the files filtering the ones that ends with .nu
// and transform them into suggestions
let output: Vec<Suggestion> = search_dirs
let output: Vec<SemanticSuggestion> = search_dirs
.into_iter()
.flat_map(|search_dir| {
let completions = file_path_completion(
@ -119,16 +121,20 @@ impl Completer for DotNuCompletion {
}
}
})
.map(move |x| Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: true,
},
append_whitespace: true,
// TODO????
kind: None,
})
})
.collect();

View File

@ -12,6 +12,8 @@ use reedline::Suggestion;
use std::path::{Path, MAIN_SEPARATOR as SEP};
use std::sync::Arc;
use super::SemanticSuggestion;
#[derive(Clone)]
pub struct FileCompletion {
engine_state: Arc<EngineState>,
@ -36,7 +38,7 @@ impl Completer for FileCompletion {
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let AdjustView {
prefix,
span,
@ -53,16 +55,20 @@ impl Completer for FileCompletion {
&self.stack,
)
.into_iter()
.map(move |x| Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x.1,
description: None,
style: x.2,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
},
append_whitespace: false,
},
append_whitespace: false,
// TODO????
kind: None,
})
.collect();
@ -70,7 +76,7 @@ impl Completer for FileCompletion {
}
// Sort results prioritizing the non hidden folders
fn sort(&self, items: Vec<Suggestion>, prefix: Vec<u8>) -> Vec<Suggestion> {
fn sort(&self, items: Vec<SemanticSuggestion>, prefix: Vec<u8>) -> Vec<SemanticSuggestion> {
let prefix_str = String::from_utf8_lossy(&prefix).to_string();
// Sort items
@ -80,15 +86,16 @@ impl Completer for FileCompletion {
SortBy::Ascending => {
sorted_items.sort_by(|a, b| {
// Ignore trailing slashes in folder names when sorting
a.value
a.suggestion
.value
.trim_end_matches(SEP)
.cmp(b.value.trim_end_matches(SEP))
.cmp(b.suggestion.value.trim_end_matches(SEP))
});
}
SortBy::LevenshteinDistance => {
sorted_items.sort_by(|a, b| {
let a_distance = levenshtein_distance(&prefix_str, &a.value);
let b_distance = levenshtein_distance(&prefix_str, &b.value);
let a_distance = levenshtein_distance(&prefix_str, &a.suggestion.value);
let b_distance = levenshtein_distance(&prefix_str, &b.suggestion.value);
a_distance.cmp(&b_distance)
});
}
@ -96,11 +103,11 @@ impl Completer for FileCompletion {
}
// Separate the results between hidden and non hidden
let mut hidden: Vec<Suggestion> = vec![];
let mut non_hidden: Vec<Suggestion> = vec![];
let mut hidden: Vec<SemanticSuggestion> = vec![];
let mut non_hidden: Vec<SemanticSuggestion> = vec![];
for item in sorted_items.into_iter() {
let item_path = Path::new(&item.value);
let item_path = Path::new(&item.suggestion.value);
if let Some(value) = item_path.file_name() {
if let Some(value) = value.to_str() {

View File

@ -7,6 +7,8 @@ use nu_protocol::{
use reedline::Suggestion;
use super::SemanticSuggestion;
#[derive(Clone)]
pub struct FlagCompletion {
expression: Expression,
@ -27,7 +29,7 @@ impl Completer for FlagCompletion {
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
// Check if it's a flag
if let Expr::Call(call) = &self.expression.expr {
let decl = working_set.get_decl(call.decl_id);
@ -43,16 +45,20 @@ impl Completer for FlagCompletion {
named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
},
append_whitespace: true,
// TODO????
kind: None,
});
}
}
@ -66,16 +72,20 @@ impl Completer for FlagCompletion {
named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
style: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
},
append_whitespace: true,
},
append_whitespace: true,
// TODO????
kind: None,
});
}
}

View File

@ -10,7 +10,7 @@ mod file_completions;
mod flag_completions;
mod variable_completions;
pub use base::Completer;
pub use base::{Completer, SemanticSuggestion, SuggestionKind};
pub use command_completions::CommandCompletion;
pub use completer::NuCompleter;
pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy};

View File

@ -9,7 +9,7 @@ use reedline::Suggestion;
use std::str;
use std::sync::Arc;
use super::MatchAlgorithm;
use super::{MatchAlgorithm, SemanticSuggestion, SuggestionKind};
#[derive(Clone)]
pub struct VariableCompletion {
@ -41,7 +41,7 @@ impl Completer for VariableCompletion {
offset: usize,
_: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let mut output = vec![];
let builtins = ["$nu", "$in", "$env"];
let var_str = std::str::from_utf8(&self.var_context.0).unwrap_or("");
@ -75,7 +75,7 @@ impl Completer for VariableCompletion {
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.value.as_bytes(),
suggestion.suggestion.value.as_bytes(),
&prefix,
) {
output.push(suggestion);
@ -92,13 +92,16 @@ impl Completer for VariableCompletion {
env_var.0.as_bytes(),
&prefix,
) {
output.push(Suggestion {
value: env_var.0,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: env_var.0,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(env_var.1.get_type())),
});
}
}
@ -121,7 +124,7 @@ impl Completer for VariableCompletion {
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.value.as_bytes(),
suggestion.suggestion.value.as_bytes(),
&prefix,
) {
output.push(suggestion);
@ -144,7 +147,7 @@ impl Completer for VariableCompletion {
{
if options.match_algorithm.matches_u8_insensitive(
options.case_sensitive,
suggestion.value.as_bytes(),
suggestion.suggestion.value.as_bytes(),
&prefix,
) {
output.push(suggestion);
@ -163,13 +166,17 @@ impl Completer for VariableCompletion {
builtin.as_bytes(),
&prefix,
) {
output.push(Suggestion {
value: builtin.to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: builtin.to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
// TODO is there a way to get the VarId to get the type???
kind: None,
});
}
}
@ -186,13 +193,18 @@ impl Completer for VariableCompletion {
v.0,
&prefix,
) {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
});
}
}
@ -208,13 +220,18 @@ impl Completer for VariableCompletion {
v.0,
&prefix,
) {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(SuggestionKind::Type(
working_set.get_variable(*v.1).ty.clone(),
)),
});
}
}
@ -232,21 +249,25 @@ fn nested_suggestions(
val: Value,
sublevels: Vec<Vec<u8>>,
current_span: reedline::Span,
) -> Vec<Suggestion> {
let mut output: Vec<Suggestion> = vec![];
) -> Vec<SemanticSuggestion> {
let mut output: Vec<SemanticSuggestion> = vec![];
let value = recursive_value(val, sublevels);
let kind = SuggestionKind::Type(value.get_type());
match value {
Value::Record { val, .. } => {
// Add all the columns as completion
for (col, _) in val.into_iter() {
output.push(Suggestion {
value: col,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: col,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
});
}
@ -255,13 +276,16 @@ fn nested_suggestions(
Value::LazyRecord { val, .. } => {
// Add all the columns as completion
for column_name in val.column_names() {
output.push(Suggestion {
value: column_name.to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: column_name.to_string(),
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
});
}
@ -269,13 +293,16 @@ fn nested_suggestions(
}
Value::List { vals, .. } => {
for column_name in get_columns(vals.as_slice()) {
output.push(Suggestion {
value: column_name,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
output.push(SemanticSuggestion {
suggestion: Suggestion {
value: column_name,
description: None,
style: None,
extra: None,
span: current_span,
append_whitespace: false,
},
kind: Some(kind.clone()),
});
}

View File

@ -15,7 +15,7 @@ mod util;
mod validation;
pub use commands::add_cli_context;
pub use completions::{FileCompletion, NuCompleter};
pub use completions::{FileCompletion, NuCompleter, SemanticSuggestion, SuggestionKind};
pub use config_files::eval_config_contents;
pub use eval_cmds::evaluate_commands;
pub use eval_file::evaluate_file;

View File

@ -11,18 +11,18 @@ use std::{
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
use lsp_types::{
request::{Completion, GotoDefinition, HoverRequest, Request},
CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams,
GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind,
OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, Url,
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location,
MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit,
Url,
};
use miette::{IntoDiagnostic, Result};
use nu_cli::NuCompleter;
use nu_cli::{NuCompleter, SuggestionKind};
use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
DeclId, Span, Value, VarId,
};
use reedline::Completer;
use ropey::Rope;
mod diagnostics;
@ -559,7 +559,8 @@ impl LanguageServer {
let location =
Self::lsp_position_to_location(&params.text_document_position.position, rope_of_file);
let results = completer.complete(&rope_of_file.to_string()[..location], location);
let results =
completer.fetch_completions_at(&rope_of_file.to_string()[..location], location);
if results.is_empty() {
None
} else {
@ -568,17 +569,18 @@ impl LanguageServer {
.into_iter()
.map(|r| {
let mut start = params.text_document_position.position;
start.character -= (r.span.end - r.span.start) as u32;
start.character -= (r.suggestion.span.end - r.suggestion.span.start) as u32;
CompletionItem {
label: r.value.clone(),
detail: r.description,
label: r.suggestion.value.clone(),
detail: r.suggestion.description,
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.value,
new_text: r.suggestion.value,
})),
..Default::default()
}
@ -587,12 +589,28 @@ impl LanguageServer {
))
}
}
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),
_ => None,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_json_diff::assert_json_eq;
use assert_json_diff::{assert_json_eq, assert_json_include};
use lsp_types::{
notification::{
DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification,
@ -1078,7 +1096,8 @@ mod tests {
"start": { "character": 5, "line": 2 },
"end": { "character": 9, "line": 2 }
}
}
},
"kind": 6
}
])
);
@ -1115,7 +1134,8 @@ mod tests {
"end": { "line": 0, "character": 8 },
},
"newText": "config nu"
}
},
"kind": 3
}
])
);
@ -1152,7 +1172,45 @@ mod tests {
"end": { "line": 0, "character": 14 },
},
"newText": "str trim"
}
},
"kind": 3
}
])
);
}
#[test]
fn complete_keyword() {
let (client_connection, _recv) = initialize_language_server();
let mut script = fixtures();
script.push("lsp");
script.push("completion");
script.push("keyword.nu");
let script = Url::from_file_path(script).unwrap();
open_unchecked(&client_connection, script.clone());
let resp = complete(&client_connection, script, 0, 2);
let result = if let Message::Response(response) = resp {
response.result
} else {
panic!()
};
assert_json_include!(
actual: result,
expected: serde_json::json!([
{
"label": "def",
"textEdit": {
"newText": "def",
"range": {
"start": { "character": 0, "line": 0 },
"end": { "character": 2, "line": 0 }
}
},
"kind": 14
}
])
);

View File

@ -2,7 +2,7 @@ use crate::{ast::Call, Alias, BlockId, Example, IoStream, PipelineData, ShellErr
use super::{EngineState, Stack, StateWorkingSet};
#[derive(Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum CommandType {
Builtin,
Custom,

View File

@ -4,7 +4,8 @@ use lru::LruCache;
use super::cached_file::CachedFile;
use super::{usage::build_usage, usage::Usage, StateDelta};
use super::{
Command, EnvVars, OverlayFrame, ScopeFrame, Stack, Variable, Visibility, DEFAULT_OVERLAY_NAME,
Command, CommandType, EnvVars, OverlayFrame, ScopeFrame, Stack, Variable, Visibility,
DEFAULT_OVERLAY_NAME,
};
use crate::ast::Block;
use crate::debugger::{Debugger, NoopDebugger};
@ -733,7 +734,7 @@ impl EngineState {
&self,
predicate: impl Fn(&[u8]) -> bool,
ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>)> {
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![];
for overlay_frame in self.active_overlays(&[]).rev() {
@ -743,7 +744,11 @@ impl EngineState {
if ignore_deprecated && command.signature().category == Category::Removed {
continue;
}
output.push((decl.0.clone(), Some(command.usage().to_string())));
output.push((
decl.0.clone(),
Some(command.usage().to_string()),
command.command_type(),
));
}
}
}

View File

@ -1,4 +1,5 @@
use super::cached_file::CachedFile;
use super::CommandType;
use super::{
usage::build_usage, Command, EngineState, OverlayFrame, StateDelta, Variable, VirtualPath,
Visibility, PWD_ENV,
@ -708,7 +709,7 @@ impl<'a> StateWorkingSet<'a> {
&self,
predicate: impl Fn(&[u8]) -> bool,
ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>)> {
) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![];
for scope_frame in self.delta.scope.iter().rev() {
@ -721,7 +722,11 @@ impl<'a> StateWorkingSet<'a> {
if ignore_deprecated && command.signature().category == Category::Removed {
continue;
}
output.push((decl.0.clone(), Some(command.usage().to_string())));
output.push((
decl.0.clone(),
Some(command.usage().to_string()),
command.command_type(),
));
}
}
}

View File

@ -0,0 +1 @@
de