Skip to content

Commit

Permalink
Send LSP Completion Item Kind (nushell#11443)
Browse files Browse the repository at this point in the history
# 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 nushell#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)
  • Loading branch information
schrieveslaach authored Mar 25, 2024
1 parent d1a8992 commit e7bdd08
Show file tree
Hide file tree
Showing 16 changed files with 383 additions and 198 deletions.
32 changes: 27 additions & 5 deletions crates/nu-cli/src/completions/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,53 @@ 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;

// Sort items
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 => {}
};

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()
}
}
}
67 changes: 41 additions & 26 deletions crates/nu-cli/src/completions/command_completions.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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)>,
Expand Down Expand Up @@ -83,21 +88,24 @@ 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);

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<_>>();

Expand All @@ -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,
.map(move |x| SemanticSuggestion {
suggestion: Suggestion {
value: x,
description: None,
style: None,
extra: None,
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.clone().into_iter().map(|x| x.value).collect();
results.iter().map(|x| x.suggestion.value.clone()).collect();

for external in results_external {
if results_strings.contains(&external.value) {
results.push(Suggestion {
value: format!("^{}", external.value),
description: None,
style: None,
extra: None,
span: external.span,
append_whitespace: true,
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)
Expand All @@ -151,7 +166,7 @@ impl Completer for CommandCompletion {
offset: usize,
pos: usize,
options: &CompletionOptions,
) -> Vec<Suggestion> {
) -> Vec<SemanticSuggestion> {
let last = self
.flattened
.iter()
Expand Down
47 changes: 31 additions & 16 deletions crates/nu-cli/src/completions/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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())),
});
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down
21 changes: 14 additions & 7 deletions crates/nu-cli/src/completions/custom_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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()
}
Loading

0 comments on commit e7bdd08

Please sign in to comment.