Skip to content

internal: Make CompletionItem more POD-like #14165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 25 additions & 65 deletions crates/ide-completion/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ use crate::{
/// editor pop-up. It is basically a POD with various properties. To construct a
/// [`CompletionItem`], use [`Builder::new`] method and the [`Builder`] struct.
#[derive(Clone)]
#[non_exhaustive]
pub struct CompletionItem {
/// Label in the completion pop up which identifies completion.
label: SmolStr,
pub label: SmolStr,
/// Range of identifier that is being completed.
///
/// It should be used primarily for UI, but we also use this to convert
Expand All @@ -29,33 +30,33 @@ pub struct CompletionItem {
/// `source_range` must contain the completion offset. `text_edit` should
/// start with what `source_range` points to, or VSCode will filter out the
/// completion silently.
source_range: TextRange,
pub source_range: TextRange,
/// What happens when user selects this item.
///
/// Typically, replaces `source_range` with new identifier.
text_edit: TextEdit,
is_snippet: bool,
pub text_edit: TextEdit,
pub is_snippet: bool,

/// What item (struct, function, etc) are we completing.
kind: CompletionItemKind,
pub kind: CompletionItemKind,

/// Lookup is used to check if completion item indeed can complete current
/// ident.
///
/// That is, in `foo.bar$0` lookup of `abracadabra` will be accepted (it
/// contains `bar` sub sequence), and `quux` will rejected.
lookup: Option<SmolStr>,
pub lookup: Option<SmolStr>,

/// Additional info to show in the UI pop up.
detail: Option<String>,
documentation: Option<Documentation>,
pub detail: Option<String>,
pub documentation: Option<Documentation>,

/// Whether this item is marked as deprecated
deprecated: bool,
pub deprecated: bool,

/// If completing a function call, ask the editor to show parameter popup
/// after completion.
trigger_call_info: bool,
pub trigger_call_info: bool,

/// We use this to sort completion. Relevance records facts like "do the
/// types align precisely?". We can't sort by relevances directly, they are
Expand All @@ -64,36 +65,39 @@ pub struct CompletionItem {
/// Note that Relevance ignores fuzzy match score. We compute Relevance for
/// all possible items, and then separately build an ordered completion list
/// based on relevance and fuzzy matching with the already typed identifier.
relevance: CompletionRelevance,
pub relevance: CompletionRelevance,

/// Indicates that a reference or mutable reference to this variable is a
/// possible match.
ref_match: Option<(Mutability, TextSize)>,
// FIXME: We shouldn't expose Mutability here (that is HIR types at all), its fine for now though
// until we have more splitting completions in which case we should think about
// generalizing this. See https://github.com/rust-lang/rust-analyzer/issues/12571
pub ref_match: Option<(Mutability, TextSize)>,

/// The import data to add to completion's edits.
import_to_add: SmallVec<[LocatedImport; 1]>,
pub import_to_add: SmallVec<[LocatedImport; 1]>,
}

// We use custom debug for CompletionItem to make snapshot tests more readable.
impl fmt::Debug for CompletionItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("CompletionItem");
s.field("label", &self.label()).field("source_range", &self.source_range());
if self.text_edit().len() == 1 {
let atom = &self.text_edit().iter().next().unwrap();
s.field("label", &self.label).field("source_range", &self.source_range);
if self.text_edit.len() == 1 {
let atom = &self.text_edit.iter().next().unwrap();
s.field("delete", &atom.delete);
s.field("insert", &atom.insert);
} else {
s.field("text_edit", &self.text_edit);
}
s.field("kind", &self.kind());
if self.lookup() != self.label() {
s.field("kind", &self.kind);
if self.lookup() != self.label {
s.field("lookup", &self.lookup());
}
if let Some(detail) = self.detail() {
if let Some(detail) = &self.detail {
s.field("detail", &detail);
}
if let Some(documentation) = self.documentation() {
if let Some(documentation) = &self.documentation {
s.field("documentation", &documentation);
}
if self.deprecated {
Expand Down Expand Up @@ -351,51 +355,11 @@ impl CompletionItem {
}
}

/// What user sees in pop-up in the UI.
pub fn label(&self) -> &str {
&self.label
}
pub fn source_range(&self) -> TextRange {
self.source_range
}

pub fn text_edit(&self) -> &TextEdit {
&self.text_edit
}
/// Whether `text_edit` is a snippet (contains `$0` markers).
pub fn is_snippet(&self) -> bool {
self.is_snippet
}

/// Short one-line additional information, like a type
pub fn detail(&self) -> Option<&str> {
self.detail.as_deref()
}
/// A doc-comment
pub fn documentation(&self) -> Option<Documentation> {
self.documentation.clone()
}
/// What string is used for filtering.
pub fn lookup(&self) -> &str {
self.lookup.as_deref().unwrap_or(&self.label)
}

pub fn kind(&self) -> CompletionItemKind {
self.kind
}

pub fn deprecated(&self) -> bool {
self.deprecated
}

pub fn relevance(&self) -> CompletionRelevance {
self.relevance
}

pub fn trigger_call_info(&self) -> bool {
self.trigger_call_info
}

pub fn ref_match(&self) -> Option<(String, text_edit::Indel, CompletionRelevance)> {
// Relevance of the ref match should be the same as the original
// match, but with exact type match set because self.ref_match
Expand All @@ -405,16 +369,12 @@ impl CompletionItem {

self.ref_match.map(|(mutability, offset)| {
(
format!("&{}{}", mutability.as_keyword_for_ref(), self.label()),
format!("&{}{}", mutability.as_keyword_for_ref(), self.label),
text_edit::Indel::insert(offset, format!("&{}", mutability.as_keyword_for_ref())),
relevance,
)
})
}

pub fn imports_to_add(&self) -> &[LocatedImport] {
&self.import_to_add
}
}

/// A helper to make `CompletionItem`s.
Expand Down
18 changes: 9 additions & 9 deletions crates/ide-completion/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,18 +503,18 @@ mod tests {
#[track_caller]
fn check_relevance_for_kinds(ra_fixture: &str, kinds: &[CompletionItemKind], expect: Expect) {
let mut actual = get_all_items(TEST_CONFIG, ra_fixture, None);
actual.retain(|it| kinds.contains(&it.kind()));
actual.sort_by_key(|it| cmp::Reverse(it.relevance().score()));
actual.retain(|it| kinds.contains(&it.kind));
actual.sort_by_key(|it| cmp::Reverse(it.relevance.score()));
check_relevance_(actual, expect);
}

#[track_caller]
fn check_relevance(ra_fixture: &str, expect: Expect) {
let mut actual = get_all_items(TEST_CONFIG, ra_fixture, None);
actual.retain(|it| it.kind() != CompletionItemKind::Snippet);
actual.retain(|it| it.kind() != CompletionItemKind::Keyword);
actual.retain(|it| it.kind() != CompletionItemKind::BuiltinType);
actual.sort_by_key(|it| cmp::Reverse(it.relevance().score()));
actual.retain(|it| it.kind != CompletionItemKind::Snippet);
actual.retain(|it| it.kind != CompletionItemKind::Keyword);
actual.retain(|it| it.kind != CompletionItemKind::BuiltinType);
actual.sort_by_key(|it| cmp::Reverse(it.relevance.score()));
check_relevance_(actual, expect);
}

Expand All @@ -525,9 +525,9 @@ mod tests {
.flat_map(|it| {
let mut items = vec![];

let tag = it.kind().tag();
let relevance = display_relevance(it.relevance());
items.push(format!("{tag} {} {relevance}\n", it.label()));
let tag = it.kind.tag();
let relevance = display_relevance(it.relevance);
items.push(format!("{tag} {} {relevance}\n", it.label));

if let Some((label, _indel, relevance)) = it.ref_match() {
let relevance = display_relevance(relevance);
Expand Down
30 changes: 15 additions & 15 deletions crates/ide-completion/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ fn completion_list_with_config(
let items = get_all_items(config, ra_fixture, trigger_character);
let items = items
.into_iter()
.filter(|it| it.kind() != CompletionItemKind::BuiltinType || it.label() == "u32")
.filter(|it| include_keywords || it.kind() != CompletionItemKind::Keyword)
.filter(|it| include_keywords || it.kind() != CompletionItemKind::Snippet)
.sorted_by_key(|it| (it.kind(), it.label().to_owned(), it.detail().map(ToOwned::to_owned)))
.filter(|it| it.kind != CompletionItemKind::BuiltinType || it.label == "u32")
.filter(|it| include_keywords || it.kind != CompletionItemKind::Keyword)
.filter(|it| include_keywords || it.kind != CompletionItemKind::Snippet)
.sorted_by_key(|it| (it.kind, it.label.clone(), it.detail.as_ref().map(ToOwned::to_owned)))
.collect();
render_completion_list(items)
}
Expand All @@ -138,8 +138,8 @@ pub(crate) fn do_completion_with_config(
) -> Vec<CompletionItem> {
get_all_items(config, code, None)
.into_iter()
.filter(|c| c.kind() == kind)
.sorted_by(|l, r| l.label().cmp(r.label()))
.filter(|c| c.kind == kind)
.sorted_by(|l, r| l.label.cmp(&r.label))
.collect()
}

Expand All @@ -148,18 +148,18 @@ fn render_completion_list(completions: Vec<CompletionItem>) -> String {
s.chars().count()
}
let label_width =
completions.iter().map(|it| monospace_width(it.label())).max().unwrap_or_default().min(22);
completions.iter().map(|it| monospace_width(&it.label)).max().unwrap_or_default().min(22);
completions
.into_iter()
.map(|it| {
let tag = it.kind().tag();
let var_name = format!("{tag} {}", it.label());
let tag = it.kind.tag();
let var_name = format!("{tag} {}", it.label);
let mut buf = var_name;
if let Some(detail) = it.detail() {
let width = label_width.saturating_sub(monospace_width(it.label()));
if let Some(detail) = it.detail {
let width = label_width.saturating_sub(monospace_width(&it.label));
format_to!(buf, "{:width$} {}", "", detail, width = width);
}
if it.deprecated() {
if it.deprecated {
format_to!(buf, " DEPRECATED");
}
format_to!(buf, "\n");
Expand Down Expand Up @@ -191,13 +191,13 @@ pub(crate) fn check_edit_with_config(
.unwrap_or_else(|| panic!("can't find {what:?} completion in {completions:#?}"));
let mut actual = db.file_text(position.file_id).to_string();

let mut combined_edit = completion.text_edit().to_owned();
let mut combined_edit = completion.text_edit.clone();

resolve_completion_edits(
&db,
&config,
position,
completion.imports_to_add().iter().filter_map(|import_edit| {
completion.import_to_add.iter().filter_map(|import_edit| {
let import_path = &import_edit.import_path;
let import_name = import_path.segments().last()?;
Some((import_path.to_string(), import_name.to_string()))
Expand Down Expand Up @@ -225,7 +225,7 @@ pub(crate) fn get_all_items(
.map_or_else(Vec::default, Into::into);
// validate
res.iter().for_each(|it| {
let sr = it.source_range();
let sr = it.source_range;
assert!(
sr.contains_inclusive(position.offset),
"source range {sr:?} does not contain the offset {:?} of the completion request: {it:?}",
Expand Down
38 changes: 21 additions & 17 deletions crates/rust-analyzer/src/to_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ pub(crate) fn completion_items(
tdpp: lsp_types::TextDocumentPositionParams,
items: Vec<CompletionItem>,
) -> Vec<lsp_types::CompletionItem> {
let max_relevance = items.iter().map(|it| it.relevance().score()).max().unwrap_or_default();
let max_relevance = items.iter().map(|it| it.relevance.score()).max().unwrap_or_default();
let mut res = Vec::with_capacity(items.len());
for item in items {
completion_item(&mut res, config, line_index, &tdpp, max_relevance, item);
Expand All @@ -235,14 +235,17 @@ fn completion_item(
item: CompletionItem,
) {
let insert_replace_support = config.insert_replace_support().then_some(tdpp.position);
let ref_match = item.ref_match();
let lookup = item.lookup().to_string();

let mut additional_text_edits = Vec::new();

// LSP does not allow arbitrary edits in completion, so we have to do a
// non-trivial mapping here.
let text_edit = {
let mut text_edit = None;
let source_range = item.source_range();
for indel in item.text_edit() {
let source_range = item.source_range;
for indel in item.text_edit {
if indel.delete.contains_range(source_range) {
// Extract this indel as the main edit
text_edit = Some(if indel.delete == source_range {
Expand All @@ -265,23 +268,23 @@ fn completion_item(
text_edit.unwrap()
};

let insert_text_format = item.is_snippet().then_some(lsp_types::InsertTextFormat::SNIPPET);
let tags = item.deprecated().then(|| vec![lsp_types::CompletionItemTag::DEPRECATED]);
let command = if item.trigger_call_info() && config.client_commands().trigger_parameter_hints {
let insert_text_format = item.is_snippet.then_some(lsp_types::InsertTextFormat::SNIPPET);
let tags = item.deprecated.then(|| vec![lsp_types::CompletionItemTag::DEPRECATED]);
let command = if item.trigger_call_info && config.client_commands().trigger_parameter_hints {
Some(command::trigger_parameter_hints())
} else {
None
};

let mut lsp_item = lsp_types::CompletionItem {
label: item.label().to_string(),
detail: item.detail().map(|it| it.to_string()),
filter_text: Some(item.lookup().to_string()),
kind: Some(completion_item_kind(item.kind())),
label: item.label.to_string(),
detail: item.detail.map(|it| it.to_string()),
filter_text: Some(lookup),
kind: Some(completion_item_kind(item.kind)),
text_edit: Some(text_edit),
additional_text_edits: Some(additional_text_edits),
documentation: item.documentation().map(documentation),
deprecated: Some(item.deprecated()),
documentation: item.documentation.map(documentation),
deprecated: Some(item.deprecated),
tags,
command,
insert_text_format,
Expand All @@ -295,12 +298,13 @@ fn completion_item(
});
}

set_score(&mut lsp_item, max_relevance, item.relevance());
set_score(&mut lsp_item, max_relevance, item.relevance);

if config.completion().enable_imports_on_the_fly {
if let imports @ [_, ..] = item.imports_to_add() {
let imports: Vec<_> = imports
.iter()
if !item.import_to_add.is_empty() {
let imports: Vec<_> = item
.import_to_add
.into_iter()
.filter_map(|import_edit| {
let import_path = &import_edit.import_path;
let import_name = import_path.segments().last()?;
Expand All @@ -317,7 +321,7 @@ fn completion_item(
}
}

if let Some((label, indel, relevance)) = item.ref_match() {
if let Some((label, indel, relevance)) = ref_match {
let mut lsp_item_with_ref = lsp_types::CompletionItem { label, ..lsp_item.clone() };
lsp_item_with_ref
.additional_text_edits
Expand Down