mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-30 22:01:37 +00:00
Improve inlay hint resolution reliability
This commit is contained in:
parent
13ac53e73d
commit
5ca5d52697
20 changed files with 282 additions and 216 deletions
|
@ -15,7 +15,7 @@ use span::{Edition, EditionedFileId};
|
|||
use stdx::never;
|
||||
use syntax::{
|
||||
ast::{self, AstNode},
|
||||
match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
|
||||
match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize, WalkEvent,
|
||||
};
|
||||
use text_edit::TextEdit;
|
||||
|
||||
|
@ -36,6 +36,192 @@ mod implicit_static;
|
|||
mod param_name;
|
||||
mod range_exclusive;
|
||||
|
||||
// Feature: Inlay Hints
|
||||
//
|
||||
// rust-analyzer shows additional information inline with the source code.
|
||||
// Editors usually render this using read-only virtual text snippets interspersed with code.
|
||||
//
|
||||
// rust-analyzer by default shows hints for
|
||||
//
|
||||
// * types of local variables
|
||||
// * names of function arguments
|
||||
// * names of const generic parameters
|
||||
// * types of chained expressions
|
||||
//
|
||||
// Optionally, one can enable additional hints for
|
||||
//
|
||||
// * return types of closure expressions
|
||||
// * elided lifetimes
|
||||
// * compiler inserted reborrows
|
||||
// * names of generic type and lifetime parameters
|
||||
//
|
||||
// Note: inlay hints for function argument names are heuristically omitted to reduce noise and will not appear if
|
||||
// any of the
|
||||
// link:https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L92-L99[following criteria]
|
||||
// are met:
|
||||
//
|
||||
// * the parameter name is a suffix of the function's name
|
||||
// * the argument is a qualified constructing or call expression where the qualifier is an ADT
|
||||
// * exact argument<->parameter match(ignoring leading underscore) or parameter is a prefix/suffix
|
||||
// of argument with _ splitting it off
|
||||
// * the parameter name starts with `ra_fixture`
|
||||
// * the parameter name is a
|
||||
// link:https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L200[well known name]
|
||||
// in a unary function
|
||||
// * the parameter name is a
|
||||
// link:https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L201[single character]
|
||||
// in a unary function
|
||||
//
|
||||
// image::https://user-images.githubusercontent.com/48062697/113020660-b5f98b80-917a-11eb-8d70-3be3fd558cdd.png[]
|
||||
pub(crate) fn inlay_hints(
|
||||
db: &RootDatabase,
|
||||
file_id: FileId,
|
||||
range_limit: Option<TextRange>,
|
||||
config: &InlayHintsConfig,
|
||||
) -> Vec<InlayHint> {
|
||||
let _p = tracing::info_span!("inlay_hints").entered();
|
||||
let sema = Semantics::new(db);
|
||||
let file_id = sema
|
||||
.attach_first_edition(file_id)
|
||||
.unwrap_or_else(|| EditionedFileId::current_edition(file_id));
|
||||
let file = sema.parse(file_id);
|
||||
let file = file.syntax();
|
||||
|
||||
let mut acc = Vec::new();
|
||||
|
||||
let Some(scope) = sema.scope(file) else {
|
||||
return acc;
|
||||
};
|
||||
let famous_defs = FamousDefs(&sema, scope.krate());
|
||||
|
||||
let parent_impl = &mut None;
|
||||
let hints = |node| hints(&mut acc, parent_impl, &famous_defs, config, file_id, node);
|
||||
match range_limit {
|
||||
// FIXME: This can miss some hints that require the parent of the range to calculate
|
||||
Some(range) => match file.covering_element(range) {
|
||||
NodeOrToken::Token(_) => return acc,
|
||||
NodeOrToken::Node(n) => n
|
||||
.preorder()
|
||||
.filter(|event| matches!(event, WalkEvent::Enter(node) if range.intersect(node.text_range()).is_some()))
|
||||
.for_each(hints),
|
||||
},
|
||||
None => file.preorder().for_each(hints),
|
||||
};
|
||||
|
||||
acc
|
||||
}
|
||||
|
||||
pub(crate) fn inlay_hints_resolve(
|
||||
db: &RootDatabase,
|
||||
file_id: FileId,
|
||||
resolve_range: TextRange,
|
||||
hash: u64,
|
||||
config: &InlayHintsConfig,
|
||||
hasher: impl Fn(&InlayHint) -> u64,
|
||||
) -> Option<InlayHint> {
|
||||
let _p = tracing::info_span!("inlay_hints_resolve").entered();
|
||||
let sema = Semantics::new(db);
|
||||
let file_id = sema
|
||||
.attach_first_edition(file_id)
|
||||
.unwrap_or_else(|| EditionedFileId::current_edition(file_id));
|
||||
let file = sema.parse(file_id);
|
||||
let file = file.syntax();
|
||||
|
||||
let scope = sema.scope(file)?;
|
||||
let famous_defs = FamousDefs(&sema, scope.krate());
|
||||
let mut acc = Vec::new();
|
||||
|
||||
let parent_impl = &mut None;
|
||||
let hints = |node| hints(&mut acc, parent_impl, &famous_defs, config, file_id, node);
|
||||
|
||||
let mut res = file.clone();
|
||||
let res = loop {
|
||||
res = match res.child_or_token_at_range(resolve_range) {
|
||||
Some(NodeOrToken::Node(n)) if n.text_range() == resolve_range => break n,
|
||||
Some(NodeOrToken::Node(n)) => n,
|
||||
_ => break res,
|
||||
};
|
||||
};
|
||||
res.preorder().for_each(hints);
|
||||
acc.into_iter().find(|hint| hasher(hint) == hash)
|
||||
}
|
||||
|
||||
fn hints(
|
||||
hints: &mut Vec<InlayHint>,
|
||||
parent_impl: &mut Option<ast::Impl>,
|
||||
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
|
||||
config: &InlayHintsConfig,
|
||||
file_id: EditionedFileId,
|
||||
node: WalkEvent<SyntaxNode>,
|
||||
) {
|
||||
let node = match node {
|
||||
WalkEvent::Enter(node) => node,
|
||||
WalkEvent::Leave(n) => {
|
||||
if ast::Impl::can_cast(n.kind()) {
|
||||
parent_impl.take();
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
closing_brace::hints(hints, sema, config, file_id, node.clone());
|
||||
if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
|
||||
generic_param::hints(hints, sema, config, any_has_generic_args);
|
||||
}
|
||||
|
||||
match_ast! {
|
||||
match node {
|
||||
ast::Expr(expr) => {
|
||||
chaining::hints(hints, famous_defs, config, file_id, &expr);
|
||||
adjustment::hints(hints, famous_defs, config, file_id, &expr);
|
||||
match expr {
|
||||
ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
|
||||
ast::Expr::MethodCallExpr(it) => {
|
||||
param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
|
||||
}
|
||||
ast::Expr::ClosureExpr(it) => {
|
||||
closure_captures::hints(hints, famous_defs, config, file_id, it.clone());
|
||||
closure_ret::hints(hints, famous_defs, config, file_id, it)
|
||||
},
|
||||
ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, file_id, it),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
ast::Pat(it) => {
|
||||
binding_mode::hints(hints, famous_defs, config, file_id, &it);
|
||||
match it {
|
||||
ast::Pat::IdentPat(it) => {
|
||||
bind_pat::hints(hints, famous_defs, config, file_id, &it);
|
||||
}
|
||||
ast::Pat::RangePat(it) => {
|
||||
range_exclusive::hints(hints, famous_defs, config, file_id, it);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(())
|
||||
},
|
||||
ast::Item(it) => match it {
|
||||
// FIXME: record impl lifetimes so they aren't being reused in assoc item lifetime inlay hints
|
||||
ast::Item::Impl(impl_) => {
|
||||
*parent_impl = Some(impl_);
|
||||
None
|
||||
},
|
||||
ast::Item::Fn(it) => {
|
||||
implicit_drop::hints(hints, famous_defs, config, file_id, &it);
|
||||
fn_lifetime_fn::hints(hints, famous_defs, config, file_id, it)
|
||||
},
|
||||
// static type elisions
|
||||
ast::Item::Static(it) => implicit_static::hints(hints, famous_defs, config, file_id, Either::Left(it)),
|
||||
ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, file_id, Either::Right(it)),
|
||||
ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, file_id, it),
|
||||
_ => None,
|
||||
},
|
||||
// FIXME: fn-ptr type, dyn fn type, and trait object type elisions
|
||||
ast::Type(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InlayHintsConfig {
|
||||
pub render_colons: bool,
|
||||
|
@ -162,6 +348,9 @@ pub struct InlayHint {
|
|||
pub label: InlayHintLabel,
|
||||
/// Text edit to apply when "accepting" this inlay hint.
|
||||
pub text_edit: Option<TextEdit>,
|
||||
/// Range to recompute inlay hints when trying to resolve for this hint. If this is none, the
|
||||
/// hint does not support resolving.
|
||||
pub resolve_parent: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for InlayHint {
|
||||
|
@ -186,6 +375,7 @@ impl InlayHint {
|
|||
position: InlayHintPosition::After,
|
||||
pad_left: false,
|
||||
pad_right: false,
|
||||
resolve_parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,11 +388,12 @@ impl InlayHint {
|
|||
position: InlayHintPosition::Before,
|
||||
pad_left: false,
|
||||
pad_right: false,
|
||||
resolve_parent: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_resolve(&self) -> bool {
|
||||
self.text_edit.is_some() || self.label.needs_resolve()
|
||||
pub fn needs_resolve(&self) -> Option<TextRange> {
|
||||
self.resolve_parent.filter(|_| self.text_edit.is_some() || self.label.needs_resolve())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,190 +625,6 @@ fn label_of_ty(
|
|||
Some(r)
|
||||
}
|
||||
|
||||
fn ty_to_text_edit(
|
||||
sema: &Semantics<'_, RootDatabase>,
|
||||
node_for_hint: &SyntaxNode,
|
||||
ty: &hir::Type,
|
||||
offset_to_insert: TextSize,
|
||||
prefix: String,
|
||||
) -> Option<TextEdit> {
|
||||
let scope = sema.scope(node_for_hint)?;
|
||||
// FIXME: Limit the length and bail out on excess somehow?
|
||||
let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
|
||||
|
||||
let mut builder = TextEdit::builder();
|
||||
builder.insert(offset_to_insert, prefix);
|
||||
builder.insert(offset_to_insert, rendered);
|
||||
Some(builder.finish())
|
||||
}
|
||||
|
||||
// Feature: Inlay Hints
|
||||
//
|
||||
// rust-analyzer shows additional information inline with the source code.
|
||||
// Editors usually render this using read-only virtual text snippets interspersed with code.
|
||||
//
|
||||
// rust-analyzer by default shows hints for
|
||||
//
|
||||
// * types of local variables
|
||||
// * names of function arguments
|
||||
// * names of const generic parameters
|
||||
// * types of chained expressions
|
||||
//
|
||||
// Optionally, one can enable additional hints for
|
||||
//
|
||||
// * return types of closure expressions
|
||||
// * elided lifetimes
|
||||
// * compiler inserted reborrows
|
||||
// * names of generic type and lifetime parameters
|
||||
//
|
||||
// Note: inlay hints for function argument names are heuristically omitted to reduce noise and will not appear if
|
||||
// any of the
|
||||
// link:https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L92-L99[following criteria]
|
||||
// are met:
|
||||
//
|
||||
// * the parameter name is a suffix of the function's name
|
||||
// * the argument is a qualified constructing or call expression where the qualifier is an ADT
|
||||
// * exact argument<->parameter match(ignoring leading underscore) or parameter is a prefix/suffix
|
||||
// of argument with _ splitting it off
|
||||
// * the parameter name starts with `ra_fixture`
|
||||
// * the parameter name is a
|
||||
// link:https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L200[well known name]
|
||||
// in a unary function
|
||||
// * the parameter name is a
|
||||
// link:https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L201[single character]
|
||||
// in a unary function
|
||||
//
|
||||
// image::https://user-images.githubusercontent.com/48062697/113020660-b5f98b80-917a-11eb-8d70-3be3fd558cdd.png[]
|
||||
pub(crate) fn inlay_hints(
|
||||
db: &RootDatabase,
|
||||
file_id: FileId,
|
||||
range_limit: Option<TextRange>,
|
||||
config: &InlayHintsConfig,
|
||||
) -> Vec<InlayHint> {
|
||||
let _p = tracing::info_span!("inlay_hints").entered();
|
||||
let sema = Semantics::new(db);
|
||||
let file_id = sema
|
||||
.attach_first_edition(file_id)
|
||||
.unwrap_or_else(|| EditionedFileId::current_edition(file_id));
|
||||
let file = sema.parse(file_id);
|
||||
let file = file.syntax();
|
||||
|
||||
let mut acc = Vec::new();
|
||||
|
||||
if let Some(scope) = sema.scope(file) {
|
||||
let famous_defs = FamousDefs(&sema, scope.krate());
|
||||
|
||||
let hints = |node| hints(&mut acc, &famous_defs, config, file_id, node);
|
||||
match range_limit {
|
||||
Some(range) => match file.covering_element(range) {
|
||||
NodeOrToken::Token(_) => return acc,
|
||||
NodeOrToken::Node(n) => n
|
||||
.descendants()
|
||||
.filter(|descendant| range.intersect(descendant.text_range()).is_some())
|
||||
.for_each(hints),
|
||||
},
|
||||
None => file.descendants().for_each(hints),
|
||||
};
|
||||
}
|
||||
|
||||
acc
|
||||
}
|
||||
|
||||
pub(crate) fn inlay_hints_resolve(
|
||||
db: &RootDatabase,
|
||||
file_id: FileId,
|
||||
position: TextSize,
|
||||
hash: u64,
|
||||
config: &InlayHintsConfig,
|
||||
hasher: impl Fn(&InlayHint) -> u64,
|
||||
) -> Option<InlayHint> {
|
||||
let _p = tracing::info_span!("inlay_hints_resolve").entered();
|
||||
let sema = Semantics::new(db);
|
||||
let file_id = sema
|
||||
.attach_first_edition(file_id)
|
||||
.unwrap_or_else(|| EditionedFileId::current_edition(file_id));
|
||||
let file = sema.parse(file_id);
|
||||
let file = file.syntax();
|
||||
|
||||
let scope = sema.scope(file)?;
|
||||
let famous_defs = FamousDefs(&sema, scope.krate());
|
||||
let mut acc = Vec::new();
|
||||
|
||||
let hints = |node| hints(&mut acc, &famous_defs, config, file_id, node);
|
||||
let token = file.token_at_offset(position).left_biased()?;
|
||||
if let Some(parent_block) = token.parent_ancestors().find_map(ast::BlockExpr::cast) {
|
||||
parent_block.syntax().descendants().for_each(hints)
|
||||
} else if let Some(parent_item) = token.parent_ancestors().find_map(ast::Item::cast) {
|
||||
parent_item.syntax().descendants().for_each(hints)
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
|
||||
acc.into_iter().find(|hint| hasher(hint) == hash)
|
||||
}
|
||||
|
||||
fn hints(
|
||||
hints: &mut Vec<InlayHint>,
|
||||
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
|
||||
config: &InlayHintsConfig,
|
||||
file_id: EditionedFileId,
|
||||
node: SyntaxNode,
|
||||
) {
|
||||
closing_brace::hints(hints, sema, config, file_id, node.clone());
|
||||
if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
|
||||
generic_param::hints(hints, sema, config, any_has_generic_args);
|
||||
}
|
||||
match_ast! {
|
||||
match node {
|
||||
ast::Expr(expr) => {
|
||||
chaining::hints(hints, famous_defs, config, file_id, &expr);
|
||||
adjustment::hints(hints, sema, config, file_id, &expr);
|
||||
match expr {
|
||||
ast::Expr::CallExpr(it) => param_name::hints(hints, sema, config, ast::Expr::from(it)),
|
||||
ast::Expr::MethodCallExpr(it) => {
|
||||
param_name::hints(hints, sema, config, ast::Expr::from(it))
|
||||
}
|
||||
ast::Expr::ClosureExpr(it) => {
|
||||
closure_captures::hints(hints, famous_defs, config, file_id, it.clone());
|
||||
closure_ret::hints(hints, famous_defs, config, file_id, it)
|
||||
},
|
||||
ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, config, it),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
ast::Pat(it) => {
|
||||
binding_mode::hints(hints, sema, config, &it);
|
||||
match it {
|
||||
ast::Pat::IdentPat(it) => {
|
||||
bind_pat::hints(hints, famous_defs, config, file_id, &it);
|
||||
}
|
||||
ast::Pat::RangePat(it) => {
|
||||
range_exclusive::hints(hints, config, it);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(())
|
||||
},
|
||||
ast::Item(it) => match it {
|
||||
// FIXME: record impl lifetimes so they aren't being reused in assoc item lifetime inlay hints
|
||||
ast::Item::Impl(_) => None,
|
||||
ast::Item::Fn(it) => {
|
||||
implicit_drop::hints(hints, sema, config, file_id, &it);
|
||||
fn_lifetime_fn::hints(hints, config, it)
|
||||
},
|
||||
// static type elisions
|
||||
ast::Item::Static(it) => implicit_static::hints(hints, config, Either::Left(it)),
|
||||
ast::Item::Const(it) => implicit_static::hints(hints, config, Either::Right(it)),
|
||||
ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, file_id, it),
|
||||
_ => None,
|
||||
},
|
||||
// FIXME: fn-ptr type, dyn fn type, and trait object type elisions
|
||||
ast::Type(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks if the type is an Iterator from std::iter and returns the iterator trait and the item type of the concrete iterator.
|
||||
fn hint_iterator(
|
||||
sema: &Semantics<'_, RootDatabase>,
|
||||
|
@ -653,6 +660,23 @@ fn hint_iterator(
|
|||
None
|
||||
}
|
||||
|
||||
fn ty_to_text_edit(
|
||||
sema: &Semantics<'_, RootDatabase>,
|
||||
node_for_hint: &SyntaxNode,
|
||||
ty: &hir::Type,
|
||||
offset_to_insert: TextSize,
|
||||
prefix: String,
|
||||
) -> Option<TextEdit> {
|
||||
let scope = sema.scope(node_for_hint)?;
|
||||
// FIXME: Limit the length and bail out on excess somehow?
|
||||
let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
|
||||
|
||||
let mut builder = TextEdit::builder();
|
||||
builder.insert(offset_to_insert, prefix);
|
||||
builder.insert(offset_to_insert, rendered);
|
||||
Some(builder.finish())
|
||||
}
|
||||
|
||||
fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
|
||||
matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue