diff --git a/crates/ra_ide/src/completion.rs b/crates/ra_ide/src/completion.rs index 4a1a2a04a7d1..19bc4321c187 100644 --- a/crates/ra_ide/src/completion.rs +++ b/crates/ra_ide/src/completion.rs @@ -29,7 +29,7 @@ use crate::{ }; pub use crate::completion::completion_item::{ - CompletionItem, CompletionItemKind, InsertTextFormat, + CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat, }; #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/ra_ide/src/completion/complete_dot.rs b/crates/ra_ide/src/completion/complete_dot.rs index f433faef36a8..44288f92e694 100644 --- a/crates/ra_ide/src/completion/complete_dot.rs +++ b/crates/ra_ide/src/completion/complete_dot.rs @@ -2,9 +2,11 @@ use hir::{HasVisibility, Type}; -use crate::completion::completion_item::CompletionKind; use crate::{ - completion::{completion_context::CompletionContext, completion_item::Completions}, + completion::{ + completion_context::CompletionContext, + completion_item::{CompletionKind, Completions}, + }, CompletionItem, }; use rustc_hash::FxHashSet; @@ -103,6 +105,237 @@ mod tests { ); } + #[test] + fn test_struct_field_completion_in_func_call() { + assert_debug_snapshot!( + do_ref_completion( + r" + struct A { another_field: i64, the_field: u32, my_string: String } + fn test(my_param: u32) -> u32 { my_param } + fn foo(a: A) { + test(a.<|>) + } + ", + ), + @r###" + [ + CompletionItem { + label: "another_field", + source_range: [201; 201), + delete: [201; 201), + insert: "another_field", + kind: Field, + detail: "i64", + }, + CompletionItem { + label: "my_string", + source_range: [201; 201), + delete: [201; 201), + insert: "my_string", + kind: Field, + detail: "{unknown}", + }, + CompletionItem { + label: "the_field", + source_range: [201; 201), + delete: [201; 201), + insert: "the_field", + kind: Field, + detail: "u32", + score: TypeMatch, + }, + ] + "### + ); + } + + #[test] + fn test_struct_field_completion_in_func_call_with_type_and_name() { + assert_debug_snapshot!( + do_ref_completion( + r" + struct A { another_field: i64, another_good_type: u32, the_field: u32 } + fn test(the_field: u32) -> u32 { the_field } + fn foo(a: A) { + test(a.<|>) + } + ", + ), + @r###" + [ + CompletionItem { + label: "another_field", + source_range: [208; 208), + delete: [208; 208), + insert: "another_field", + kind: Field, + detail: "i64", + }, + CompletionItem { + label: "another_good_type", + source_range: [208; 208), + delete: [208; 208), + insert: "another_good_type", + kind: Field, + detail: "u32", + score: TypeMatch, + }, + CompletionItem { + label: "the_field", + source_range: [208; 208), + delete: [208; 208), + insert: "the_field", + kind: Field, + detail: "u32", + score: TypeAndNameMatch, + }, + ] + "### + ); + } + + #[test] + fn test_struct_field_completion_in_record_lit() { + assert_debug_snapshot!( + do_ref_completion( + r" + struct A { another_field: i64, another_good_type: u32, the_field: u32 } + struct B { my_string: String, my_vec: Vec, the_field: u32 } + fn foo(a: A) { + let b = B { + the_field: a.<|> + }; + } + ", + ), + @r###" + [ + CompletionItem { + label: "another_field", + source_range: [270; 270), + delete: [270; 270), + insert: "another_field", + kind: Field, + detail: "i64", + }, + CompletionItem { + label: "another_good_type", + source_range: [270; 270), + delete: [270; 270), + insert: "another_good_type", + kind: Field, + detail: "u32", + score: TypeMatch, + }, + CompletionItem { + label: "the_field", + source_range: [270; 270), + delete: [270; 270), + insert: "the_field", + kind: Field, + detail: "u32", + score: TypeAndNameMatch, + }, + ] + "### + ); + } + + #[test] + fn test_struct_field_completion_in_record_lit_and_fn_call() { + assert_debug_snapshot!( + do_ref_completion( + r" + struct A { another_field: i64, another_good_type: u32, the_field: u32 } + struct B { my_string: String, my_vec: Vec, the_field: u32 } + fn test(the_field: i64) -> i64 { the_field } + fn foo(a: A) { + let b = B { + the_field: test(a.<|>) + }; + } + ", + ), + @r###" + [ + CompletionItem { + label: "another_field", + source_range: [336; 336), + delete: [336; 336), + insert: "another_field", + kind: Field, + detail: "i64", + score: TypeMatch, + }, + CompletionItem { + label: "another_good_type", + source_range: [336; 336), + delete: [336; 336), + insert: "another_good_type", + kind: Field, + detail: "u32", + }, + CompletionItem { + label: "the_field", + source_range: [336; 336), + delete: [336; 336), + insert: "the_field", + kind: Field, + detail: "u32", + }, + ] + "### + ); + } + + #[test] + fn test_struct_field_completion_in_fn_call_and_record_lit() { + assert_debug_snapshot!( + do_ref_completion( + r" + struct A { another_field: i64, another_good_type: u32, the_field: u32 } + struct B { my_string: String, my_vec: Vec, the_field: u32 } + fn test(the_field: i64) -> i64 { the_field } + fn foo(a: A) { + test(B { + the_field: a.<|> + }); + } + ", + ), + @r###" + [ + CompletionItem { + label: "another_field", + source_range: [328; 328), + delete: [328; 328), + insert: "another_field", + kind: Field, + detail: "i64", + }, + CompletionItem { + label: "another_good_type", + source_range: [328; 328), + delete: [328; 328), + insert: "another_good_type", + kind: Field, + detail: "u32", + score: TypeMatch, + }, + CompletionItem { + label: "the_field", + source_range: [328; 328), + delete: [328; 328), + insert: "the_field", + kind: Field, + detail: "u32", + score: TypeAndNameMatch, + }, + ] + "### + ); + } + #[test] fn test_struct_field_completion_self() { assert_debug_snapshot!( diff --git a/crates/ra_ide/src/completion/completion_context.rs b/crates/ra_ide/src/completion/completion_context.rs index cfc5c34df085..dd7c8a873dbd 100644 --- a/crates/ra_ide/src/completion/completion_context.rs +++ b/crates/ra_ide/src/completion/completion_context.rs @@ -21,6 +21,7 @@ pub(crate) struct CompletionContext<'a> { pub(super) db: &'a RootDatabase, pub(super) config: &'a CompletionConfig, pub(super) offset: TextUnit, + pub(super) file_position: FilePosition, /// The token before the cursor, in the original file. pub(super) original_token: SyntaxToken, /// The token before the cursor, in the macro-expanded file. @@ -31,6 +32,7 @@ pub(crate) struct CompletionContext<'a> { pub(super) use_item_syntax: Option, pub(super) record_lit_syntax: Option, pub(super) record_pat_syntax: Option, + pub(super) record_field_syntax: Option, pub(super) impl_def: Option, pub(super) is_param: bool, /// If a name-binding or reference to a const in a pattern. @@ -88,12 +90,14 @@ impl<'a> CompletionContext<'a> { original_token, token, offset: position.offset, + file_position: position, krate, name_ref_syntax: None, function_syntax: None, use_item_syntax: None, record_lit_syntax: None, record_pat_syntax: None, + record_field_syntax: None, impl_def: None, is_param: false, is_pat_binding_or_const: false, @@ -279,6 +283,14 @@ impl<'a> CompletionContext<'a> { .take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE) .find_map(ast::FnDef::cast); + self.record_field_syntax = self + .sema + .ancestors_with_macros(self.token.parent()) + .take_while(|it| { + it.kind() != SOURCE_FILE && it.kind() != MODULE && it.kind() != CALL_EXPR + }) + .find_map(ast::RecordField::cast); + let parent = match name_ref.syntax().parent() { Some(it) => it, None => return, diff --git a/crates/ra_ide/src/completion/completion_item.rs b/crates/ra_ide/src/completion/completion_item.rs index bc0f1aff58b6..e17586aa5bf8 100644 --- a/crates/ra_ide/src/completion/completion_item.rs +++ b/crates/ra_ide/src/completion/completion_item.rs @@ -51,6 +51,9 @@ pub struct CompletionItem { /// If completing a function call, ask the editor to show parameter popup /// after completion. trigger_call_info: bool, + + /// Score is usefull to pre select or display in better order completion items + score: Option, } // We use custom debug for CompletionItem to make `insta`'s diffs more readable. @@ -80,6 +83,9 @@ impl fmt::Debug for CompletionItem { if self.deprecated { s.field("deprecated", &true); } + if let Some(score) = &self.score { + s.field("score", score); + } if self.trigger_call_info { s.field("trigger_call_info", &true); } @@ -147,6 +153,7 @@ impl CompletionItem { text_edit: None, deprecated: None, trigger_call_info: None, + score: None, } } /// What user sees in pop-up in the UI. @@ -186,6 +193,14 @@ impl CompletionItem { self.deprecated } + pub fn score(&self) -> Option { + self.score.clone() + } + + pub fn set_score(&mut self, score: CompletionScore) { + self.score = Some(score); + } + pub fn trigger_call_info(&self) -> bool { self.trigger_call_info } @@ -206,6 +221,7 @@ pub(crate) struct Builder { text_edit: Option, deprecated: Option, trigger_call_info: Option, + score: Option, } impl Builder { @@ -235,6 +251,7 @@ impl Builder { completion_kind: self.completion_kind, deprecated: self.deprecated.unwrap_or(false), trigger_call_info: self.trigger_call_info.unwrap_or(false), + score: self.score, } } pub(crate) fn lookup_by(mut self, lookup: impl Into) -> Builder { @@ -285,6 +302,11 @@ impl Builder { self.deprecated = Some(deprecated); self } + #[allow(unused)] + pub(crate) fn set_score(mut self, score: CompletionScore) -> Builder { + self.score = Some(score); + self + } pub(crate) fn trigger_call_info(mut self) -> Builder { self.trigger_call_info = Some(true); self @@ -297,6 +319,14 @@ impl<'a> Into for Builder { } } +#[derive(Debug, Clone)] +pub enum CompletionScore { + /// If only type match + TypeMatch, + /// If type and name match + TypeAndNameMatch, +} + /// Represents an in-progress set of completions being built. #[derive(Debug, Default)] pub(crate) struct Completions { diff --git a/crates/ra_ide/src/completion/presentation.rs b/crates/ra_ide/src/completion/presentation.rs index 2189cef658e3..f8dac1d54178 100644 --- a/crates/ra_ide/src/completion/presentation.rs +++ b/crates/ra_ide/src/completion/presentation.rs @@ -6,12 +6,13 @@ use stdx::SepBy; use test_utils::tested_by; use crate::{ + call_info::call_info, completion::{ completion_item::Builder, CompletionContext, CompletionItem, CompletionItemKind, CompletionKind, Completions, }, display::{const_label, macro_label, type_label, FunctionSignature}, - RootDatabase, + CompletionScore, RootDatabase, }; impl Completions { @@ -22,7 +23,7 @@ impl Completions { ty: &Type, ) { let is_deprecated = is_deprecated(field, ctx.db); - CompletionItem::new( + let mut completion_item = CompletionItem::new( CompletionKind::Reference, ctx.source_range(), field.name(ctx.db).to_string(), @@ -31,7 +32,11 @@ impl Completions { .detail(ty.display(ctx.db).to_string()) .set_documentation(field.docs(ctx.db)) .set_deprecated(is_deprecated) - .add_to(self); + .build(); + + compute_score(&mut completion_item, ctx); + + self.add(completion_item); } pub(crate) fn add_tuple_field(&mut self, ctx: &CompletionContext, field: usize, ty: &Type) { @@ -300,6 +305,42 @@ impl Completions { } } +pub(crate) fn compute_score(completion_item: &mut CompletionItem, ctx: &CompletionContext) { + let (active_name, active_type) = if let Some(record_field) = &ctx.record_field_syntax { + if let Some((struct_field, _)) = ctx.sema.resolve_record_field(record_field) { + ( + struct_field.name(ctx.db).to_string(), + struct_field.signature_ty(ctx.db).display(ctx.db).to_string(), + ) + } else { + return; + } + } else if let Some(call_info) = call_info(ctx.db, ctx.file_position) { + if call_info.active_parameter_type().is_some() + && call_info.active_parameter_name().is_some() + { + (call_info.active_parameter_name().unwrap(), call_info.active_parameter_type().unwrap()) + } else { + return; + } + } else { + return; + }; + + // Compute score + // For the same type + if let Some(a_parameter_type) = completion_item.detail() { + if &active_type == a_parameter_type { + // If same type + same name then go top position + if active_name == completion_item.label() { + completion_item.set_score(CompletionScore::TypeAndNameMatch); + } else { + completion_item.set_score(CompletionScore::TypeMatch); + } + } + } +} + enum Params { Named(Vec), Anonymous(usize), diff --git a/crates/ra_ide/src/display/function_signature.rs b/crates/ra_ide/src/display/function_signature.rs index b967a6816804..b5e2785fe042 100644 --- a/crates/ra_ide/src/display/function_signature.rs +++ b/crates/ra_ide/src/display/function_signature.rs @@ -36,6 +36,8 @@ pub struct FunctionSignature { pub parameters: Vec, /// Parameter names of the function pub parameter_names: Vec, + /// Parameter types of the function + pub parameter_types: Vec, /// Optional return type pub ret_type: Option, /// Where predicates @@ -62,14 +64,20 @@ impl FunctionSignature { return None; }; - let params = st - .fields(db) - .into_iter() - .map(|field: hir::StructField| { - let ty = field.signature_ty(db); - format!("{}", ty.display(db)) - }) - .collect(); + let mut params = vec![]; + let mut parameter_types = vec![]; + for field in st.fields(db).into_iter() { + let ty = field.signature_ty(db); + let raw_param = format!("{}", ty.display(db)); + + if let Some(param_type) = raw_param.split(':').nth(1) { + parameter_types.push(param_type[1..].to_string()); + } else { + // useful when you have tuple struct + parameter_types.push(raw_param.clone()); + } + params.push(raw_param); + } Some( FunctionSignature { @@ -79,6 +87,7 @@ impl FunctionSignature { ret_type: node.name().map(|n| n.text().to_string()), parameters: params, parameter_names: vec![], + parameter_types, generic_parameters: generic_parameters(&node), where_predicates: where_predicates(&node), doc: None, @@ -99,15 +108,21 @@ impl FunctionSignature { let name = format!("{}::{}", parent_name, variant.name(db)); - let params = variant - .fields(db) - .into_iter() - .map(|field: hir::StructField| { - let name = field.name(db); - let ty = field.signature_ty(db); - format!("{}: {}", name, ty.display(db)) - }) - .collect(); + let mut params = vec![]; + let mut parameter_types = vec![]; + for field in variant.fields(db).into_iter() { + let ty = field.signature_ty(db); + let raw_param = format!("{}", ty.display(db)); + if let Some(param_type) = raw_param.split(':').nth(1) { + parameter_types.push(param_type[1..].to_string()); + } else { + // The unwrap_or_else is useful when you have tuple + parameter_types.push(raw_param); + } + let name = field.name(db); + + params.push(format!("{}: {}", name, ty.display(db))); + } Some( FunctionSignature { @@ -117,6 +132,7 @@ impl FunctionSignature { ret_type: None, parameters: params, parameter_names: vec![], + parameter_types, generic_parameters: vec![], where_predicates: vec![], doc: None, @@ -139,6 +155,7 @@ impl FunctionSignature { ret_type: None, parameters: params, parameter_names: vec![], + parameter_types: vec![], generic_parameters: vec![], where_predicates: vec![], doc: None, @@ -151,18 +168,27 @@ impl FunctionSignature { impl From<&'_ ast::FnDef> for FunctionSignature { fn from(node: &ast::FnDef) -> FunctionSignature { - fn param_list(node: &ast::FnDef) -> (bool, Vec) { + fn param_list(node: &ast::FnDef) -> (bool, Vec, Vec) { let mut res = vec![]; + let mut res_types = vec![]; let mut has_self_param = false; if let Some(param_list) = node.param_list() { if let Some(self_param) = param_list.self_param() { has_self_param = true; - res.push(self_param.syntax().text().to_string()) + let raw_param = self_param.syntax().text().to_string(); + + res_types.push( + raw_param.split(':').nth(1).unwrap_or_else(|| " Self")[1..].to_string(), + ); + res.push(raw_param); } res.extend(param_list.params().map(|param| param.syntax().text().to_string())); + res_types.extend(param_list.params().map(|param| { + param.syntax().text().to_string().split(':').nth(1).unwrap()[1..].to_string() + })); } - (has_self_param, res) + (has_self_param, res, res_types) } fn param_name_list(node: &ast::FnDef) -> Vec { @@ -192,7 +218,7 @@ impl From<&'_ ast::FnDef> for FunctionSignature { res } - let (has_self_param, parameters) = param_list(node); + let (has_self_param, parameters, parameter_types) = param_list(node); FunctionSignature { kind: CallableKind::Function, @@ -204,6 +230,7 @@ impl From<&'_ ast::FnDef> for FunctionSignature { .map(|n| n.syntax().text().to_string()), parameters, parameter_names: param_name_list(node), + parameter_types, generic_parameters: generic_parameters(node), where_predicates: where_predicates(node), // docs are processed separately diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 5599f143f20b..ddaa30a1606b 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -67,7 +67,9 @@ use crate::display::ToNav; pub use crate::{ assists::{Assist, AssistId}, call_hierarchy::CallItem, - completion::{CompletionConfig, CompletionItem, CompletionItemKind, InsertTextFormat}, + completion::{ + CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat, + }, diagnostics::Severity, display::{file_structure, FunctionSignature, NavigationTarget, StructureNode}, expand_macro::ExpandedMacro, @@ -127,6 +129,21 @@ pub struct CallInfo { pub active_parameter: Option, } +impl CallInfo { + pub fn active_parameter_type(&self) -> Option { + if let Some(id) = self.active_parameter { + return self.signature.parameter_types.get(id).map(|param_ty| param_ty.clone()); + } + None + } + pub fn active_parameter_name(&self) -> Option { + if let Some(id) = self.active_parameter { + return self.signature.parameter_names.get(id).map(|param_ty| param_ty.clone()); + } + None + } +} + /// `AnalysisHost` stores the current state of the world. #[derive(Debug)] pub struct AnalysisHost { diff --git a/crates/rust-analyzer/src/conv.rs b/crates/rust-analyzer/src/conv.rs index 8d2360cc8dbe..7e30956cc303 100644 --- a/crates/rust-analyzer/src/conv.rs +++ b/crates/rust-analyzer/src/conv.rs @@ -9,10 +9,10 @@ use lsp_types::{ TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, WorkspaceEdit, }; use ra_ide::{ - translate_offset_with_edit, CompletionItem, CompletionItemKind, FileId, FilePosition, - FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HighlightModifier, HighlightTag, - InlayHint, InlayKind, InsertTextFormat, LineCol, LineIndex, NavigationTarget, RangeInfo, - ReferenceAccess, Severity, SourceChange, SourceFileEdit, + translate_offset_with_edit, CompletionItem, CompletionItemKind, CompletionScore, FileId, + FilePosition, FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HighlightModifier, + HighlightTag, InlayHint, InlayKind, InsertTextFormat, LineCol, LineIndex, NavigationTarget, + RangeInfo, ReferenceAccess, Severity, SourceChange, SourceFileEdit, }; use ra_syntax::{SyntaxKind, TextRange, TextUnit}; use ra_text_edit::{AtomTextEdit, TextEdit}; @@ -114,10 +114,10 @@ impl Conv for Severity { } } -impl ConvWith<(&LineIndex, LineEndings)> for CompletionItem { +impl ConvWith<(&LineIndex, LineEndings, &mut usize)> for CompletionItem { type Output = ::lsp_types::CompletionItem; - fn conv_with(self, ctx: (&LineIndex, LineEndings)) -> ::lsp_types::CompletionItem { + fn conv_with(self, ctx: (&LineIndex, LineEndings, &mut usize)) -> ::lsp_types::CompletionItem { let mut additional_text_edits = Vec::new(); let mut text_edit = None; // LSP does not allow arbitrary edits in completion, so we have to do a @@ -125,7 +125,7 @@ impl ConvWith<(&LineIndex, LineEndings)> for CompletionItem { for atom_edit in self.text_edit().as_atoms() { if self.source_range().is_subrange(&atom_edit.delete) { text_edit = Some(if atom_edit.delete == self.source_range() { - atom_edit.conv_with(ctx) + atom_edit.conv_with((ctx.0, ctx.1)) } else { assert!(self.source_range().end() == atom_edit.delete.end()); let range1 = @@ -133,12 +133,12 @@ impl ConvWith<(&LineIndex, LineEndings)> for CompletionItem { let range2 = self.source_range(); let edit1 = AtomTextEdit::replace(range1, String::new()); let edit2 = AtomTextEdit::replace(range2, atom_edit.insert.clone()); - additional_text_edits.push(edit1.conv_with(ctx)); - edit2.conv_with(ctx) + additional_text_edits.push(edit1.conv_with((ctx.0, ctx.1))); + edit2.conv_with((ctx.0, ctx.1)) }) } else { assert!(self.source_range().intersection(&atom_edit.delete).is_none()); - additional_text_edits.push(atom_edit.conv_with(ctx)); + additional_text_edits.push(atom_edit.conv_with((ctx.0, ctx.1))); } } let text_edit = text_edit.unwrap(); @@ -165,6 +165,15 @@ impl ConvWith<(&LineIndex, LineEndings)> for CompletionItem { ..Default::default() }; + if let Some(score) = self.score() { + match score { + CompletionScore::TypeAndNameMatch => res.preselect = Some(true), + CompletionScore::TypeMatch => {} + } + res.sort_text = Some(format!("{:02}", *ctx.2)); + *ctx.2 += 1; + } + if self.deprecated() { res.tags = Some(vec![lsp_types::CompletionItemTag::Deprecated]) } diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index 41d9fe344d4d..ee669f383216 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs @@ -423,8 +423,11 @@ pub fn handle_completion( }; let line_index = world.analysis().file_line_index(position.file_id)?; let line_endings = world.file_line_endings(position.file_id); - let items: Vec = - items.into_iter().map(|item| item.conv_with((&line_index, line_endings))).collect(); + let mut count_sort_text_item = 0usize; + let items: Vec = items + .into_iter() + .map(|item| item.conv_with((&line_index, line_endings, &mut count_sort_text_item))) + .collect(); Ok(Some(items.into())) }