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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,8 @@ use nu_protocol::{
use reedline::Suggestion; use reedline::Suggestion;
use super::SemanticSuggestion;
#[derive(Clone)] #[derive(Clone)]
pub struct FlagCompletion { pub struct FlagCompletion {
expression: Expression, expression: Expression,
@ -27,7 +29,7 @@ impl Completer for FlagCompletion {
offset: usize, offset: usize,
_: usize, _: usize,
options: &CompletionOptions, options: &CompletionOptions,
) -> Vec<Suggestion> { ) -> Vec<SemanticSuggestion> {
// Check if it's a flag // Check if it's a flag
if let Expr::Call(call) = &self.expression.expr { if let Expr::Call(call) = &self.expression.expr {
let decl = working_set.get_decl(call.decl_id); let decl = working_set.get_decl(call.decl_id);
@ -43,7 +45,8 @@ impl Completer for FlagCompletion {
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) { if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(Suggestion { output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(), value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()), description: Some(flag_desc.to_string()),
style: None, style: None,
@ -53,6 +56,9 @@ impl Completer for FlagCompletion {
end: span.end - offset, end: span.end - offset,
}, },
append_whitespace: true, append_whitespace: true,
},
// TODO????
kind: None,
}); });
} }
} }
@ -66,7 +72,8 @@ impl Completer for FlagCompletion {
named.insert(0, b'-'); named.insert(0, b'-');
if options.match_algorithm.matches_u8(&named, &prefix) { if options.match_algorithm.matches_u8(&named, &prefix) {
output.push(Suggestion { output.push(SemanticSuggestion {
suggestion: Suggestion {
value: String::from_utf8_lossy(&named).to_string(), value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()), description: Some(flag_desc.to_string()),
style: None, style: None,
@ -76,6 +83,9 @@ impl Completer for FlagCompletion {
end: span.end - 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 flag_completions;
mod variable_completions; mod variable_completions;
pub use base::Completer; pub use base::{Completer, SemanticSuggestion, SuggestionKind};
pub use command_completions::CommandCompletion; pub use command_completions::CommandCompletion;
pub use completer::NuCompleter; pub use completer::NuCompleter;
pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy}; pub use completion_options::{CompletionOptions, MatchAlgorithm, SortBy};

View File

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

View File

@ -15,7 +15,7 @@ mod util;
mod validation; mod validation;
pub use commands::add_cli_context; 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 config_files::eval_config_contents;
pub use eval_cmds::evaluate_commands; pub use eval_cmds::evaluate_commands;
pub use eval_file::evaluate_file; pub use eval_file::evaluate_file;

View File

@ -11,18 +11,18 @@ use std::{
use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_server::{Connection, IoThreads, Message, Response, ResponseError};
use lsp_types::{ use lsp_types::{
request::{Completion, GotoDefinition, HoverRequest, Request}, request::{Completion, GotoDefinition, HoverRequest, Request},
CompletionItem, CompletionParams, CompletionResponse, CompletionTextEdit, GotoDefinitionParams, CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit,
GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location, MarkupContent, MarkupKind, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, Location,
OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit, Url, MarkupContent, MarkupKind, OneOf, Range, ServerCapabilities, TextDocumentSyncKind, TextEdit,
Url,
}; };
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use nu_cli::NuCompleter; use nu_cli::{NuCompleter, SuggestionKind};
use nu_parser::{flatten_block, parse, FlatShape}; use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
DeclId, Span, Value, VarId, DeclId, Span, Value, VarId,
}; };
use reedline::Completer;
use ropey::Rope; use ropey::Rope;
mod diagnostics; mod diagnostics;
@ -559,7 +559,8 @@ impl LanguageServer {
let location = let location =
Self::lsp_position_to_location(&params.text_document_position.position, rope_of_file); 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() { if results.is_empty() {
None None
} else { } else {
@ -568,17 +569,18 @@ impl LanguageServer {
.into_iter() .into_iter()
.map(|r| { .map(|r| {
let mut start = params.text_document_position.position; 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 { CompletionItem {
label: r.value.clone(), label: r.suggestion.value.clone(),
detail: r.description, detail: r.suggestion.description,
kind: Self::lsp_completion_item_kind(r.kind),
text_edit: Some(CompletionTextEdit::Edit(TextEdit { text_edit: Some(CompletionTextEdit::Edit(TextEdit {
range: Range { range: Range {
start, start,
end: params.text_document_position.position, end: params.text_document_position.position,
}, },
new_text: r.value, new_text: r.suggestion.value,
})), })),
..Default::default() ..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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use assert_json_diff::assert_json_eq; use assert_json_diff::{assert_json_eq, assert_json_include};
use lsp_types::{ use lsp_types::{
notification::{ notification::{
DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification, DidChangeTextDocument, DidOpenTextDocument, Exit, Initialized, Notification,
@ -1078,7 +1096,8 @@ mod tests {
"start": { "character": 5, "line": 2 }, "start": { "character": 5, "line": 2 },
"end": { "character": 9, "line": 2 } "end": { "character": 9, "line": 2 }
} }
} },
"kind": 6
} }
]) ])
); );
@ -1115,7 +1134,8 @@ mod tests {
"end": { "line": 0, "character": 8 }, "end": { "line": 0, "character": 8 },
}, },
"newText": "config nu" "newText": "config nu"
} },
"kind": 3
} }
]) ])
); );
@ -1152,7 +1172,45 @@ mod tests {
"end": { "line": 0, "character": 14 }, "end": { "line": 0, "character": 14 },
}, },
"newText": "str trim" "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}; use super::{EngineState, Stack, StateWorkingSet};
#[derive(Debug)] #[derive(Clone, Debug, PartialEq)]
pub enum CommandType { pub enum CommandType {
Builtin, Builtin,
Custom, Custom,

View File

@ -4,7 +4,8 @@ use lru::LruCache;
use super::cached_file::CachedFile; use super::cached_file::CachedFile;
use super::{usage::build_usage, usage::Usage, StateDelta}; use super::{usage::build_usage, usage::Usage, StateDelta};
use super::{ 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::ast::Block;
use crate::debugger::{Debugger, NoopDebugger}; use crate::debugger::{Debugger, NoopDebugger};
@ -733,7 +734,7 @@ impl EngineState {
&self, &self,
predicate: impl Fn(&[u8]) -> bool, predicate: impl Fn(&[u8]) -> bool,
ignore_deprecated: bool, ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>)> { ) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![]; let mut output = vec![];
for overlay_frame in self.active_overlays(&[]).rev() { for overlay_frame in self.active_overlays(&[]).rev() {
@ -743,7 +744,11 @@ impl EngineState {
if ignore_deprecated && command.signature().category == Category::Removed { if ignore_deprecated && command.signature().category == Category::Removed {
continue; 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::cached_file::CachedFile;
use super::CommandType;
use super::{ use super::{
usage::build_usage, Command, EngineState, OverlayFrame, StateDelta, Variable, VirtualPath, usage::build_usage, Command, EngineState, OverlayFrame, StateDelta, Variable, VirtualPath,
Visibility, PWD_ENV, Visibility, PWD_ENV,
@ -708,7 +709,7 @@ impl<'a> StateWorkingSet<'a> {
&self, &self,
predicate: impl Fn(&[u8]) -> bool, predicate: impl Fn(&[u8]) -> bool,
ignore_deprecated: bool, ignore_deprecated: bool,
) -> Vec<(Vec<u8>, Option<String>)> { ) -> Vec<(Vec<u8>, Option<String>, CommandType)> {
let mut output = vec![]; let mut output = vec![];
for scope_frame in self.delta.scope.iter().rev() { 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 { if ignore_deprecated && command.signature().category == Category::Removed {
continue; 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