Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/inlay_hints.rs')
-rw-r--r--crates/ide/src/inlay_hints.rs398
1 files changed, 211 insertions, 187 deletions
diff --git a/crates/ide/src/inlay_hints.rs b/crates/ide/src/inlay_hints.rs
index 6a5d5e26a4..93dd56a450 100644
--- a/crates/ide/src/inlay_hints.rs
+++ b/crates/ide/src/inlay_hints.rs
@@ -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(_)))
}