From e7bdd08a04d4983f3fd6e7a4b16a74a4f2dc1b46 Mon Sep 17 00:00:00 2001 From: Marc Schreiber Date: Mon, 25 Mar 2024 02:14:12 +0100 Subject: [PATCH] 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) --- crates/nu-cli/src/completions/base.rs | 32 +++- .../src/completions/command_completions.rs | 69 +++++---- crates/nu-cli/src/completions/completer.rs | 47 ++++-- .../src/completions/custom_completions.rs | 21 ++- .../src/completions/directory_completions.rs | 43 +++--- .../src/completions/dotnu_completions.rs | 28 ++-- .../src/completions/file_completions.rs | 43 +++--- .../src/completions/flag_completions.rs | 48 +++--- crates/nu-cli/src/completions/mod.rs | 2 +- .../src/completions/variable_completions.rs | 139 +++++++++++------- crates/nu-cli/src/lib.rs | 2 +- crates/nu-lsp/src/lib.rs | 86 +++++++++-- crates/nu-protocol/src/engine/command.rs | 2 +- crates/nu-protocol/src/engine/engine_state.rs | 11 +- .../src/engine/state_working_set.rs | 9 +- tests/fixtures/lsp/completion/keyword.nu | 1 + 16 files changed, 384 insertions(+), 199 deletions(-) create mode 100644 tests/fixtures/lsp/completion/keyword.nu diff --git a/crates/nu-cli/src/completions/base.rs b/crates/nu-cli/src/completions/base.rs index a7d492fceb..c4290b8767 100644 --- a/crates/nu-cli/src/completions/base.rs +++ b/crates/nu-cli/src/completions/base.rs @@ -13,13 +13,13 @@ pub trait Completer { offset: usize, pos: usize, options: &CompletionOptions, - ) -> Vec; + ) -> Vec; fn get_sort_by(&self) -> SortBy { SortBy::Ascending } - fn sort(&self, items: Vec, prefix: Vec) -> Vec { + fn sort(&self, items: Vec, prefix: Vec) -> Vec { 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, +} + +// 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 for SemanticSuggestion { + fn from(suggestion: Suggestion) -> Self { + Self { + suggestion, + ..Default::default() + } + } +} diff --git a/crates/nu-cli/src/completions/command_completions.rs b/crates/nu-cli/src/completions/command_completions.rs index 5e5c4f06a5..42094f9c97 100644 --- a/crates/nu-cli/src/completions/command_completions.rs +++ b/crates/nu-cli/src/completions/command_completions.rs @@ -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, flattened: Vec<(Span, FlatShape)>, @@ -83,7 +88,7 @@ impl CommandCompletion { offset: usize, find_externals: bool, match_algorithm: MatchAlgorithm, - ) -> Vec { + ) -> Vec { 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::>(); @@ -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 = - 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 = + 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 { + ) -> Vec { let last = self .flattened .iter() diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 096ed1b5f7..d11f84c772 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -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, @@ -28,6 +30,10 @@ impl NuCompleter { } } + pub fn fetch_completions_at(&mut self, line: &str, pos: usize) -> Vec { + self.completion_helper(line, pos) + } + // Process the completion for a given completer fn process_completion( &self, @@ -37,7 +43,7 @@ impl NuCompleter { new_span: Span, offset: usize, pos: usize, - ) -> Vec { + ) -> Vec { let config = self.engine_state.get_config(); let options = CompletionOptions { @@ -62,7 +68,7 @@ impl NuCompleter { spans: &[String], offset: usize, span: Span, - ) -> Option> { + ) -> Option> { 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 { + fn completion_helper(&mut self, line: &str, pos: usize) -> Vec { 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 { 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, span: Span, offset: usize, -) -> Vec { +) -> Vec { 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(), diff --git a/crates/nu-cli/src/completions/custom_completions.rs b/crates/nu-cli/src/completions/custom_completions.rs index 920eeed618..f0411fa4c3 100644 --- a/crates/nu-cli/src/completions/custom_completions.rs +++ b/crates/nu-cli/src/completions/custom_completions.rs @@ -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 { + ) -> Vec { // Line position let line_pos = pos - offset; @@ -145,15 +145,22 @@ impl Completer for CustomCompletion { } } -fn filter(prefix: &[u8], items: Vec, options: &CompletionOptions) -> Vec { +fn filter( + prefix: &[u8], + items: Vec, + options: &CompletionOptions, +) -> Vec { 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, options: &CompletionOptions) -> }, MatchAlgorithm::Fuzzy => options .match_algorithm - .matches_u8(it.value.as_bytes(), prefix), + .matches_u8(it.suggestion.value.as_bytes(), prefix), }) .collect() } diff --git a/crates/nu-cli/src/completions/directory_completions.rs b/crates/nu-cli/src/completions/directory_completions.rs index 27f2b0fa88..7db10b7308 100644 --- a/crates/nu-cli/src/completions/directory_completions.rs +++ b/crates/nu-cli/src/completions/directory_completions.rs @@ -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, @@ -35,7 +37,7 @@ impl Completer for DirectoryCompletion { offset: usize, _: usize, options: &CompletionOptions, - ) -> Vec { + ) -> Vec { 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, prefix: Vec) -> Vec { + fn sort(&self, items: Vec, prefix: Vec) -> Vec { 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 = vec![]; - let mut non_hidden: Vec = vec![]; + let mut hidden: Vec = vec![]; + let mut non_hidden: Vec = 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() { diff --git a/crates/nu-cli/src/completions/dotnu_completions.rs b/crates/nu-cli/src/completions/dotnu_completions.rs index fea082aabd..3c8abd93ff 100644 --- a/crates/nu-cli/src/completions/dotnu_completions.rs +++ b/crates/nu-cli/src/completions/dotnu_completions.rs @@ -9,6 +9,8 @@ use std::{ sync::Arc, }; +use super::SemanticSuggestion; + #[derive(Clone)] pub struct DotNuCompletion { engine_state: Arc, @@ -33,7 +35,7 @@ impl Completer for DotNuCompletion { offset: usize, _: usize, options: &CompletionOptions, - ) -> Vec { + ) -> Vec { let prefix_str = String::from_utf8_lossy(&prefix).replace('`', ""); let mut search_dirs: Vec = 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 = search_dirs + let output: Vec = 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(); diff --git a/crates/nu-cli/src/completions/file_completions.rs b/crates/nu-cli/src/completions/file_completions.rs index 9a9f607e86..f63318e113 100644 --- a/crates/nu-cli/src/completions/file_completions.rs +++ b/crates/nu-cli/src/completions/file_completions.rs @@ -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, @@ -36,7 +38,7 @@ impl Completer for FileCompletion { offset: usize, _: usize, options: &CompletionOptions, - ) -> Vec { + ) -> Vec { 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, prefix: Vec) -> Vec { + fn sort(&self, items: Vec, prefix: Vec) -> Vec { 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 = vec![]; - let mut non_hidden: Vec = vec![]; + let mut hidden: Vec = vec![]; + let mut non_hidden: Vec = 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() { diff --git a/crates/nu-cli/src/completions/flag_completions.rs b/crates/nu-cli/src/completions/flag_completions.rs index b48ca2a561..f0d3610510 100644 --- a/crates/nu-cli/src/completions/flag_completions.rs +++ b/crates/nu-cli/src/completions/flag_completions.rs @@ -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 { + ) -> Vec { // 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, }); } } diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 010995b135..20a61ee6d9 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -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}; diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs index 9877d04521..5f370125c6 100644 --- a/crates/nu-cli/src/completions/variable_completions.rs +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -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 { + ) -> Vec { 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>, current_span: reedline::Span, -) -> Vec { - let mut output: Vec = vec![]; +) -> Vec { + let mut output: Vec = 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()), }); } diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index a11356cd08..d1008d5544 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -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; diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 61f5968713..27fc3f1f58 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -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(¶ms.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, + ) -> Option { + 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 } ]) ); diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index c79b02df55..a60e412999 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -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, diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index c55ef0fd65..a54edac594 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -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, Option)> { + ) -> Vec<(Vec, Option, 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(), + )); } } } diff --git a/crates/nu-protocol/src/engine/state_working_set.rs b/crates/nu-protocol/src/engine/state_working_set.rs index 29e27c6cc5..999c7d7938 100644 --- a/crates/nu-protocol/src/engine/state_working_set.rs +++ b/crates/nu-protocol/src/engine/state_working_set.rs @@ -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, Option)> { + ) -> Vec<(Vec, Option, 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(), + )); } } } diff --git a/tests/fixtures/lsp/completion/keyword.nu b/tests/fixtures/lsp/completion/keyword.nu new file mode 100644 index 0000000000..7673daa944 --- /dev/null +++ b/tests/fixtures/lsp/completion/keyword.nu @@ -0,0 +1 @@ +de