9039: feat: Complete fields and methods with `self.` prefixed when inside methods r=matklad a=Veykril


![w65NbjkZiG](https://user-images.githubusercontent.com/3757771/119984385-a0111700-bfc1-11eb-9dbf-52fdaa4d72b5.gif)
Closes #7173

Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
This commit is contained in:
bors[bot] 2021-05-31 13:21:31 +00:00 committed by GitHub
commit e6ec860363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 170 additions and 26 deletions

View File

@ -69,18 +69,25 @@ pub(crate) fn add_all<I>(&mut self, items: I)
items.into_iter().for_each(|item| self.add(item.into()))
}
pub(crate) fn add_field(&mut self, ctx: &CompletionContext, field: hir::Field, ty: &hir::Type) {
let item = render_field(RenderContext::new(ctx), field, ty);
pub(crate) fn add_field(
&mut self,
ctx: &CompletionContext,
receiver: Option<hir::Name>,
field: hir::Field,
ty: &hir::Type,
) {
let item = render_field(RenderContext::new(ctx), receiver, field, ty);
self.add(item);
}
pub(crate) fn add_tuple_field(
&mut self,
ctx: &CompletionContext,
receiver: Option<hir::Name>,
field: usize,
ty: &hir::Type,
) {
let item = render_tuple_field(RenderContext::new(ctx), field, ty);
let item = render_tuple_field(RenderContext::new(ctx), receiver, field, ty);
self.add(item);
}
@ -132,9 +139,11 @@ pub(crate) fn add_method(
&mut self,
ctx: &CompletionContext,
func: hir::Function,
receiver: Option<hir::Name>,
local_name: Option<hir::Name>,
) {
if let Some(item) = render_method(RenderContext::new(ctx), None, local_name, func) {
if let Some(item) = render_method(RenderContext::new(ctx), None, receiver, local_name, func)
{
self.add(item)
}
}

View File

@ -1,6 +1,7 @@
//! Completes references after dot (fields and method calls).
use hir::{HasVisibility, Type};
use either::Either;
use hir::{HasVisibility, ScopeDef};
use rustc_hash::FxHashSet;
use crate::{context::CompletionContext, Completions};
@ -9,7 +10,7 @@
pub(crate) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) {
let dot_receiver = match &ctx.dot_receiver {
Some(expr) => expr,
_ => return,
_ => return complete_undotted_self(acc, ctx),
};
let receiver_ty = match ctx.sema.type_of_expr(&dot_receiver) {
@ -20,12 +21,43 @@ pub(crate) fn complete_dot(acc: &mut Completions, ctx: &CompletionContext) {
if ctx.is_call {
cov_mark::hit!(test_no_struct_field_completion_for_method_call);
} else {
complete_fields(acc, ctx, &receiver_ty);
complete_fields(ctx, &receiver_ty, |field, ty| match field {
Either::Left(field) => acc.add_field(ctx, None, field, &ty),
Either::Right(tuple_idx) => acc.add_tuple_field(ctx, None, tuple_idx, &ty),
});
}
complete_methods(acc, ctx, &receiver_ty);
complete_methods(ctx, &receiver_ty, |func| acc.add_method(ctx, func, None, None));
}
fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: &Type) {
fn complete_undotted_self(acc: &mut Completions, ctx: &CompletionContext) {
if !ctx.is_trivial_path || !ctx.config.enable_self_on_the_fly {
return;
}
ctx.scope.process_all_names(&mut |name, def| {
if let ScopeDef::Local(local) = &def {
if local.is_self(ctx.db) {
let ty = local.ty(ctx.db);
complete_fields(ctx, &ty, |field, ty| match field {
either::Either::Left(field) => {
acc.add_field(ctx, Some(name.clone()), field, &ty)
}
either::Either::Right(tuple_idx) => {
acc.add_tuple_field(ctx, Some(name.clone()), tuple_idx, &ty)
}
});
complete_methods(ctx, &ty, |func| {
acc.add_method(ctx, func, Some(name.clone()), None)
});
}
}
});
}
fn complete_fields(
ctx: &CompletionContext,
receiver: &hir::Type,
mut f: impl FnMut(Either<hir::Field, usize>, hir::Type),
) {
for receiver in receiver.autoderef(ctx.db) {
for (field, ty) in receiver.fields(ctx.db) {
if ctx.scope.module().map_or(false, |m| !field.is_visible_from(ctx.db, m)) {
@ -33,16 +65,20 @@ fn complete_fields(acc: &mut Completions, ctx: &CompletionContext, receiver: &Ty
// field is editable, we should show the completion
continue;
}
acc.add_field(ctx, field, &ty);
f(Either::Left(field), ty);
}
for (i, ty) in receiver.tuple_fields(ctx.db).into_iter().enumerate() {
// FIXME: Handle visibility
acc.add_tuple_field(ctx, i, &ty);
f(Either::Right(i), ty);
}
}
}
fn complete_methods(acc: &mut Completions, ctx: &CompletionContext, receiver: &Type) {
fn complete_methods(
ctx: &CompletionContext,
receiver: &hir::Type,
mut f: impl FnMut(hir::Function),
) {
if let Some(krate) = ctx.krate {
let mut seen_methods = FxHashSet::default();
let traits_in_scope = ctx.scope.traits_in_scope();
@ -51,7 +87,7 @@ fn complete_methods(acc: &mut Completions, ctx: &CompletionContext, receiver: &T
&& ctx.scope.module().map_or(true, |m| func.is_visible_from(ctx.db, m))
&& seen_methods.insert(func.name(ctx.db))
{
acc.add_method(ctx, func, None);
f(func);
}
None::<()>
});
@ -484,4 +520,34 @@ fn f(&mut self, v: Foo) {
"#]],
);
}
#[test]
fn completes_bare_fields_and_methods_in_methods() {
check(
r#"
struct Foo { field: i32 }
impl Foo { fn foo(&self) { $0 } }"#,
expect![[r#"
lc self &Foo
sp Self
st Foo
fd self.field i32
me self.foo() fn(&self)
"#]],
);
check(
r#"
struct Foo(i32);
impl Foo { fn foo(&mut self) { $0 } }"#,
expect![[r#"
lc self &mut Foo
sp Self
st Foo
fd self.0 i32
me self.foo() fn(&mut self)
"#]],
);
}
}

View File

@ -39,7 +39,7 @@ pub(crate) fn complete_record(acc: &mut Completions, ctx: &CompletionContext) ->
};
for (field, ty) in missing_fields {
acc.add_field(ctx, field, &ty);
acc.add_field(ctx, None, field, &ty);
}
Some(())

View File

@ -11,6 +11,7 @@ pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionC
if ctx.is_path_disallowed() || ctx.expects_item() {
return;
}
if ctx.expects_assoc_item() {
ctx.scope.process_all_names(&mut |name, def| {
if let ScopeDef::MacroDef(macro_def) = def {
@ -32,6 +33,7 @@ pub(crate) fn complete_unqualified_path(acc: &mut Completions, ctx: &CompletionC
});
return;
}
if let Some(hir::Adt::Enum(e)) =
ctx.expected_type.as_ref().and_then(|ty| ty.strip_references().as_adt())
{

View File

@ -10,6 +10,7 @@
pub struct CompletionConfig {
pub enable_postfix_completions: bool,
pub enable_imports_on_the_fly: bool,
pub enable_self_on_the_fly: bool,
pub add_call_parenthesis: bool,
pub add_call_argument_snippets: bool,
pub snippet_cap: Option<SnippetCap>,

View File

@ -25,18 +25,20 @@
pub(crate) fn render_field<'a>(
ctx: RenderContext<'a>,
receiver: Option<hir::Name>,
field: hir::Field,
ty: &hir::Type,
) -> CompletionItem {
Render::new(ctx).render_field(field, ty)
Render::new(ctx).render_field(receiver, field, ty)
}
pub(crate) fn render_tuple_field<'a>(
ctx: RenderContext<'a>,
receiver: Option<hir::Name>,
field: usize,
ty: &hir::Type,
) -> CompletionItem {
Render::new(ctx).render_tuple_field(field, ty)
Render::new(ctx).render_tuple_field(receiver, field, ty)
}
pub(crate) fn render_resolution<'a>(
@ -126,11 +128,19 @@ fn new(ctx: RenderContext<'a>) -> Render<'a> {
Render { ctx }
}
fn render_field(&self, field: hir::Field, ty: &hir::Type) -> CompletionItem {
fn render_field(
&self,
receiver: Option<hir::Name>,
field: hir::Field,
ty: &hir::Type,
) -> CompletionItem {
let is_deprecated = self.ctx.is_deprecated(field);
let name = field.name(self.ctx.db()).to_string();
let mut item =
CompletionItem::new(CompletionKind::Reference, self.ctx.source_range(), name.clone());
let mut item = CompletionItem::new(
CompletionKind::Reference,
self.ctx.source_range(),
receiver.map_or_else(|| name.clone(), |receiver| format!("{}.{}", receiver, name)),
);
item.kind(SymbolKind::Field)
.detail(ty.display(self.ctx.db()).to_string())
.set_documentation(field.docs(self.ctx.db()))
@ -151,11 +161,17 @@ fn render_field(&self, field: hir::Field, ty: &hir::Type) -> CompletionItem {
item.build()
}
fn render_tuple_field(&self, field: usize, ty: &hir::Type) -> CompletionItem {
fn render_tuple_field(
&self,
receiver: Option<hir::Name>,
field: usize,
ty: &hir::Type,
) -> CompletionItem {
let mut item = CompletionItem::new(
CompletionKind::Reference,
self.ctx.source_range(),
field.to_string(),
receiver
.map_or_else(|| field.to_string(), |receiver| format!("{}.{}", receiver, field)),
);
item.kind(SymbolKind::Field).detail(ty.display(self.ctx.db()).to_string());

View File

@ -20,23 +20,25 @@ pub(crate) fn render_fn<'a>(
fn_: hir::Function,
) -> Option<CompletionItem> {
let _p = profile::span("render_fn");
Some(FunctionRender::new(ctx, local_name, fn_, false)?.render(import_to_add))
Some(FunctionRender::new(ctx, None, local_name, fn_, false)?.render(import_to_add))
}
pub(crate) fn render_method<'a>(
ctx: RenderContext<'a>,
import_to_add: Option<ImportEdit>,
receiver: Option<hir::Name>,
local_name: Option<hir::Name>,
fn_: hir::Function,
) -> Option<CompletionItem> {
let _p = profile::span("render_method");
Some(FunctionRender::new(ctx, local_name, fn_, true)?.render(import_to_add))
Some(FunctionRender::new(ctx, receiver, local_name, fn_, true)?.render(import_to_add))
}
#[derive(Debug)]
struct FunctionRender<'a> {
ctx: RenderContext<'a>,
name: String,
receiver: Option<hir::Name>,
func: hir::Function,
ast_node: Fn,
is_method: bool,
@ -45,6 +47,7 @@ struct FunctionRender<'a> {
impl<'a> FunctionRender<'a> {
fn new(
ctx: RenderContext<'a>,
receiver: Option<hir::Name>,
local_name: Option<hir::Name>,
fn_: hir::Function,
is_method: bool,
@ -52,11 +55,14 @@ fn new(
let name = local_name.unwrap_or_else(|| fn_.name(ctx.db())).to_string();
let ast_node = fn_.source(ctx.db())?.value;
Some(FunctionRender { ctx, name, func: fn_, ast_node, is_method })
Some(FunctionRender { ctx, name, receiver, func: fn_, ast_node, is_method })
}
fn render(self, import_to_add: Option<ImportEdit>) -> CompletionItem {
fn render(mut self, import_to_add: Option<ImportEdit>) -> CompletionItem {
let params = self.params();
if let Some(receiver) = &self.receiver {
self.name = format!("{}.{}", receiver, &self.name)
}
let mut item = CompletionItem::new(
CompletionKind::Reference,
self.ctx.source_range(),
@ -148,7 +154,7 @@ fn params(&self) -> Params {
};
let mut params_pats = Vec::new();
let params_ty = if self.ctx.completion.dot_receiver.is_some() {
let params_ty = if self.ctx.completion.dot_receiver.is_some() || self.receiver.is_some() {
self.func.method_params(self.ctx.db()).unwrap_or_default()
} else {
if let Some(s) = ast_params.self_param() {
@ -253,6 +259,26 @@ fn foo(&self, x: i32) {}
fn bar(s: &S) {
s.foo(${1:x})$0
}
"#,
);
check_edit(
"self.foo",
r#"
struct S {}
impl S {
fn foo(&self, x: i32) {
$0
}
}
"#,
r#"
struct S {}
impl S {
fn foo(&self, x: i32) {
self.foo(${1:x})$0
}
}
"#,
);
}

View File

@ -19,6 +19,7 @@
pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig {
enable_postfix_completions: true,
enable_imports_on_the_fly: true,
enable_self_on_the_fly: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),

View File

@ -100,6 +100,9 @@ struct ConfigData {
/// Toggles the additional completions that automatically add imports when completed.
/// Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled.
completion_autoimport_enable: bool = "true",
/// Toggles the additional completions that automatically show method calls and field accesses
/// with `self` prefixed to them when inside a method.
completion_autoself_enable: bool = "true",
/// Whether to show native rust-analyzer diagnostics.
diagnostics_enable: bool = "true",
@ -666,6 +669,7 @@ pub fn completion(&self) -> CompletionConfig {
enable_postfix_completions: self.data.completion_postfix_enable,
enable_imports_on_the_fly: self.data.completion_autoimport_enable
&& completion_item_edit_resolve(&self.caps),
enable_self_on_the_fly: self.data.completion_autoself_enable,
add_call_parenthesis: self.data.completion_addCallParenthesis,
add_call_argument_snippets: self.data.completion_addCallArgumentSnippets,
insert_use: self.insert_use_config(),

View File

@ -132,6 +132,7 @@ fn integrated_completion_benchmark() {
let config = CompletionConfig {
enable_postfix_completions: true,
enable_imports_on_the_fly: true,
enable_self_on_the_fly: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),
@ -166,6 +167,7 @@ fn integrated_completion_benchmark() {
let config = CompletionConfig {
enable_postfix_completions: true,
enable_imports_on_the_fly: true,
enable_self_on_the_fly: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),

View File

@ -1178,6 +1178,7 @@ fn main() {
&ide::CompletionConfig {
enable_postfix_completions: true,
enable_imports_on_the_fly: true,
enable_self_on_the_fly: true,
add_call_parenthesis: true,
add_call_argument_snippets: true,
snippet_cap: SnippetCap::new(true),

View File

@ -447,3 +447,8 @@ This is cheap enough to enable in production.
Similarly, we save live object counting (`RA_COUNT=1`).
It is not cheap enough to enable in prod, and this is a bug which should be fixed.
### Configurability
rust-analyzer strives to be as configurable as possible while offering reasonable defaults where no configuration exists yet.
There will always be features that some people find more annoying than helpful, so giving the users the ability to tweak or disable these is a big part of offering a good user experience.

View File

@ -136,6 +136,12 @@ Whether to show postfix snippets like `dbg`, `if`, `not`, etc.
Toggles the additional completions that automatically add imports when completed.
Note that your client must specify the `additionalTextEdits` LSP client capability to truly have this feature enabled.
--
[[rust-analyzer.completion.autoself.enable]]rust-analyzer.completion.autoself.enable (default: `true`)::
+
--
Toggles the additional completions that automatically show method calls and field accesses
with `self` prefixed to them when inside a method.
--
[[rust-analyzer.diagnostics.enable]]rust-analyzer.diagnostics.enable (default: `true`)::
+
--

View File

@ -572,6 +572,11 @@
"default": true,
"type": "boolean"
},
"rust-analyzer.completion.autoself.enable": {
"markdownDescription": "Toggles the additional completions that automatically show method calls and field accesses\nwith `self` prefixed to them when inside a method.",
"default": true,
"type": "boolean"
},
"rust-analyzer.diagnostics.enable": {
"markdownDescription": "Whether to show native rust-analyzer diagnostics.",
"default": true,