rust/crates/ide/src/inlay_hints.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

493 lines
15 KiB
Rust
Raw Normal View History

use std::{
fmt::{self, Write},
mem::take,
};
2020-10-20 10:04:38 -05:00
use either::Either;
use hir::{known, HasVisibility, HirDisplay, HirWrite, ModuleDef, ModuleDefId, Semantics};
2022-12-16 10:13:46 -06:00
use ide_db::{base_db::FileRange, famous_defs::FamousDefs, RootDatabase};
use itertools::Itertools;
use stdx::never;
2020-08-12 11:26:51 -05:00
use syntax::{
2022-12-16 10:13:46 -06:00
ast::{self, AstNode},
match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
2019-07-19 16:20:09 -05:00
};
2019-12-21 11:45:46 -06:00
use crate::{navigation_target::TryToNav, FileId};
2020-07-16 11:07:53 -05:00
2022-12-16 10:13:46 -06:00
mod closing_brace;
mod implicit_static;
mod fn_lifetime_fn;
mod closure_ret;
mod adjustment;
mod chaining;
mod param_name;
mod binding_mode;
mod bind_pat;
#[derive(Clone, Debug, PartialEq, Eq)]
2020-03-31 09:02:55 -05:00
pub struct InlayHintsConfig {
pub location_links: bool,
pub render_colons: bool,
pub type_hints: bool,
pub parameter_hints: bool,
pub chaining_hints: bool,
pub adjustment_hints: AdjustmentHints,
pub adjustment_hints_hide_outside_unsafe: bool,
pub closure_return_type_hints: ClosureReturnTypeHints,
2022-05-14 07:26:08 -05:00
pub binding_mode_hints: bool,
2022-03-19 13:01:19 -05:00
pub lifetime_elision_hints: LifetimeElisionHints,
pub param_names_for_lifetime_elision_hints: bool,
pub hide_named_constructor_hints: bool,
pub hide_closure_initialization_hints: bool,
pub max_length: Option<usize>,
pub closing_brace_hints_min_lines: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ClosureReturnTypeHints {
Always,
WithBlock,
Never,
}
2022-03-19 13:01:19 -05:00
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LifetimeElisionHints {
Always,
SkipTrivial,
Never,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AdjustmentHints {
Always,
2022-11-04 16:59:07 -05:00
ReborrowOnly,
Never,
}
#[derive(Clone, Debug, PartialEq, Eq)]
2019-07-19 16:20:09 -05:00
pub enum InlayKind {
2022-05-14 07:26:08 -05:00
BindingModeHint,
ChainingHint,
ClosingBraceHint,
ClosureReturnTypeHint,
2022-03-18 12:11:16 -05:00
GenericParamListHint,
AdjustmentHint,
2022-03-18 12:11:16 -05:00
LifetimeHint,
ParameterHint,
TypeHint,
OpeningParenthesis,
ClosingParenthesis,
2019-07-19 16:20:09 -05:00
}
#[derive(Debug)]
pub struct InlayHint {
pub range: TextRange,
2019-07-22 13:52:47 -05:00
pub kind: InlayKind,
pub label: InlayHintLabel,
2022-05-19 06:38:37 -05:00
pub tooltip: Option<InlayTooltip>,
}
#[derive(Debug)]
pub enum InlayTooltip {
String(String),
HoverRanged(FileId, TextRange),
HoverOffset(FileId, TextSize),
2019-07-19 16:20:09 -05:00
}
#[derive(Default)]
pub struct InlayHintLabel {
pub parts: Vec<InlayHintLabelPart>,
}
impl InlayHintLabel {
pub fn as_simple_str(&self) -> Option<&str> {
match &*self.parts {
[part] => part.as_simple_str(),
_ => None,
}
}
pub fn prepend_str(&mut self, s: &str) {
match &mut *self.parts {
[part, ..] if part.as_simple_str().is_some() => part.text = format!("{s}{}", part.text),
_ => self.parts.insert(0, InlayHintLabelPart { text: s.into(), linked_location: None }),
}
}
pub fn append_str(&mut self, s: &str) {
match &mut *self.parts {
[.., part] if part.as_simple_str().is_some() => part.text.push_str(s),
_ => self.parts.push(InlayHintLabelPart { text: s.into(), linked_location: None }),
}
}
}
impl From<String> for InlayHintLabel {
fn from(s: String) -> Self {
Self { parts: vec![InlayHintLabelPart { text: s, linked_location: None }] }
}
}
impl From<&str> for InlayHintLabel {
fn from(s: &str) -> Self {
Self { parts: vec![InlayHintLabelPart { text: s.into(), linked_location: None }] }
}
}
impl fmt::Display for InlayHintLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.parts.iter().map(|part| &part.text).format(""))
}
}
impl fmt::Debug for InlayHintLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(&self.parts).finish()
}
}
pub struct InlayHintLabelPart {
pub text: String,
/// Source location represented by this label part. The client will use this to fetch the part's
/// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location
/// refers to (not necessarily the location itself).
/// When setting this, no tooltip must be set on the containing hint, or VS Code will display
/// them both.
pub linked_location: Option<FileRange>,
}
impl InlayHintLabelPart {
pub fn as_simple_str(&self) -> Option<&str> {
match self {
Self { text, linked_location: None } => Some(text),
_ => None,
}
}
}
impl fmt::Debug for InlayHintLabelPart {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.as_simple_str() {
Some(string) => string.fmt(f),
None => f
.debug_struct("InlayHintLabelPart")
.field("text", &self.text)
.field("linked_location", &self.linked_location)
.finish(),
}
}
}
#[derive(Debug)]
struct InlayHintLabelBuilder<'a> {
db: &'a RootDatabase,
result: InlayHintLabel,
last_part: String,
location_link_enabled: bool,
location: Option<FileRange>,
}
impl fmt::Write for InlayHintLabelBuilder<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.last_part.write_str(s)
}
}
impl HirWrite for InlayHintLabelBuilder<'_> {
fn start_location_link(&mut self, def: ModuleDefId) {
if !self.location_link_enabled {
return;
}
if self.location.is_some() {
never!("location link is already started");
}
self.make_new_part();
let Some(location) = ModuleDef::from(def).try_to_nav(self.db) else { return };
let location =
FileRange { file_id: location.file_id, range: location.focus_or_full_range() };
self.location = Some(location);
}
fn end_location_link(&mut self) {
if !self.location_link_enabled {
return;
}
self.make_new_part();
}
}
impl InlayHintLabelBuilder<'_> {
fn make_new_part(&mut self) {
self.result.parts.push(InlayHintLabelPart {
text: take(&mut self.last_part),
linked_location: self.location.take(),
});
}
fn finish(mut self) -> InlayHintLabel {
self.make_new_part();
self.result
}
}
fn label_of_ty(
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
config: &InlayHintsConfig,
ty: hir::Type,
) -> Option<InlayHintLabel> {
fn rec(
sema: &Semantics<'_, RootDatabase>,
famous_defs: &FamousDefs<'_, '_>,
mut max_length: Option<usize>,
ty: hir::Type,
label_builder: &mut InlayHintLabelBuilder<'_>,
) {
let iter_item_type = hint_iterator(sema, &famous_defs, &ty);
match iter_item_type {
Some(ty) => {
const LABEL_START: &str = "impl Iterator<Item = ";
const LABEL_END: &str = ">";
max_length =
max_length.map(|len| len.saturating_sub(LABEL_START.len() + LABEL_END.len()));
label_builder.write_str(LABEL_START).unwrap();
rec(sema, famous_defs, max_length, ty, label_builder);
label_builder.write_str(LABEL_END).unwrap();
}
None => {
let _ = ty.display_truncated(sema.db, max_length).write_to(label_builder);
}
};
}
let mut label_builder = InlayHintLabelBuilder {
db: sema.db,
last_part: String::new(),
location: None,
location_link_enabled: config.location_links,
result: InlayHintLabel::default(),
};
rec(sema, &famous_defs, config.max_length, ty, &mut label_builder);
let r = label_builder.finish();
Some(r)
}
// 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.
//
2022-03-18 12:11:16 -05:00
// rust-analyzer by default shows hints for
//
// * types of local variables
// * names of function arguments
// * types of chained expressions
//
2022-03-18 12:11:16 -05:00
// Optionally, one can enable additional hints for
//
// * return types of closure expressions
2022-03-19 13:01:19 -05:00
// * elided lifetimes
2022-03-20 08:41:27 -05:00
// * compiler inserted reborrows
//
// 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>,
2020-03-31 09:02:55 -05:00
config: &InlayHintsConfig,
) -> Vec<InlayHint> {
2020-08-12 09:32:36 -05:00
let _p = profile::span("inlay_hints");
let sema = Semantics::new(db);
let file = sema.parse(file_id);
2021-09-14 13:30:28 -05:00
let file = file.syntax();
2020-02-29 16:24:50 -06:00
let mut acc = Vec::new();
2022-02-11 16:48:01 -06:00
2022-05-30 06:51:48 -05:00
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) {
2022-05-30 06:51:48 -05:00
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),
};
}
2022-02-11 16:48:01 -06:00
acc
2022-02-11 16:48:01 -06:00
}
fn hints(
2022-02-11 16:48:01 -06:00
hints: &mut Vec<InlayHint>,
famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
2022-02-11 16:48:01 -06:00
config: &InlayHintsConfig,
2022-05-19 06:38:37 -05:00
file_id: FileId,
2022-02-11 16:48:01 -06:00
node: SyntaxNode,
) {
2022-12-16 10:13:46 -06:00
closing_brace::hints(hints, sema, config, file_id, node.clone());
match_ast! {
match node {
ast::Expr(expr) => {
chaining::hints(hints, famous_defs, config, file_id, &expr);
2022-12-16 10:13:46 -06:00
adjustment::hints(hints, sema, config, &expr);
match expr {
2022-12-16 10:13:46 -06:00
ast::Expr::CallExpr(it) => param_name::hints(hints, sema, config, ast::Expr::from(it)),
ast::Expr::MethodCallExpr(it) => {
2022-12-16 10:13:46 -06:00
param_name::hints(hints, sema, config, ast::Expr::from(it))
}
ast::Expr::ClosureExpr(it) => closure_ret::hints(hints, famous_defs, config, file_id, it),
// We could show reborrows for all expressions, but usually that is just noise to the user
// and the main point here is to show why "moving" a mutable reference doesn't necessarily move it
// ast::Expr::PathExpr(_) => reborrow_hints(hints, sema, config, &expr),
_ => None,
}
},
ast::Pat(it) => {
2022-12-16 10:13:46 -06:00
binding_mode::hints(hints, sema, config, &it);
if let ast::Pat::IdentPat(it) = it {
bind_pat::hints(hints, famous_defs, config, file_id, &it);
}
Some(())
},
2022-05-30 06:51:48 -05:00
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,
2022-12-16 10:13:46 -06:00
ast::Item::Fn(it) => fn_lifetime_fn::hints(hints, config, it),
2022-05-30 06:51:48 -05:00
// static type elisions
2022-12-16 10:13:46 -06:00
ast::Item::Static(it) => implicit_static::hints(hints, config, Either::Left(it)),
ast::Item::Const(it) => implicit_static::hints(hints, config, Either::Right(it)),
2022-05-30 06:51:48 -05:00
_ => None,
},
// FIXME: fn-ptr type, dyn fn type, and trait object type elisions
ast::Type(_) => None,
_ => None,
2022-05-14 07:26:08 -05:00
}
};
2019-07-19 16:20:09 -05:00
}
/// Checks if the type is an Iterator from std::iter and returns its item type.
fn hint_iterator(
2022-07-20 08:02:08 -05:00
sema: &Semantics<'_, RootDatabase>,
famous_defs: &FamousDefs<'_, '_>,
2020-10-07 06:14:12 -05:00
ty: &hir::Type,
) -> Option<hir::Type> {
let db = sema.db;
2021-05-05 15:55:12 -05:00
let strukt = ty.strip_references().as_adt()?;
2021-05-23 18:42:06 -05:00
let krate = strukt.module(db).krate();
if krate != famous_defs.core()? {
return None;
}
let iter_trait = famous_defs.core_iter_Iterator()?;
let iter_mod = famous_defs.core_iter()?;
// Assert that this struct comes from `core::iter`.
if !(strukt.visibility(db) == hir::Visibility::Public
&& strukt.module(db).path_to_root(db).contains(&iter_mod))
{
return None;
}
if ty.impls_trait(db, iter_trait, &[]) {
let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item {
2020-10-07 06:14:12 -05:00
hir::AssocItem::TypeAlias(alias) if alias.name(db) == known::Item => Some(alias),
_ => None,
})?;
if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) {
return Some(ty);
}
}
None
}
fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
}
2019-07-19 16:20:09 -05:00
#[cfg(test)]
mod tests {
use expect_test::Expect;
2022-03-18 12:11:16 -05:00
use itertools::Itertools;
2020-06-30 11:04:25 -05:00
use test_utils::extract_annotations;
2019-07-19 16:20:09 -05:00
use crate::inlay_hints::AdjustmentHints;
2022-03-19 13:01:19 -05:00
use crate::{fixture, inlay_hints::InlayHintsConfig, LifetimeElisionHints};
2020-06-30 11:04:25 -05:00
use super::ClosureReturnTypeHints;
pub(super) const DISABLED_CONFIG: InlayHintsConfig = InlayHintsConfig {
location_links: false,
render_colons: false,
type_hints: false,
parameter_hints: false,
chaining_hints: false,
2022-03-19 13:01:19 -05:00
lifetime_elision_hints: LifetimeElisionHints::Never,
closure_return_type_hints: ClosureReturnTypeHints::Never,
adjustment_hints: AdjustmentHints::Never,
adjustment_hints_hide_outside_unsafe: false,
2022-05-14 07:26:08 -05:00
binding_mode_hints: false,
hide_named_constructor_hints: false,
hide_closure_initialization_hints: false,
param_names_for_lifetime_elision_hints: false,
max_length: None,
closing_brace_hints_min_lines: None,
};
pub(super) const DISABLED_CONFIG_WITH_LINKS: InlayHintsConfig =
InlayHintsConfig { location_links: true, ..DISABLED_CONFIG };
pub(super) const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig {
type_hints: true,
parameter_hints: true,
chaining_hints: true,
closure_return_type_hints: ClosureReturnTypeHints::WithBlock,
2022-05-14 07:26:08 -05:00
binding_mode_hints: true,
2022-03-19 13:01:19 -05:00
lifetime_elision_hints: LifetimeElisionHints::Always,
..DISABLED_CONFIG_WITH_LINKS
};
#[track_caller]
pub(super) fn check(ra_fixture: &str) {
check_with_config(TEST_CONFIG, ra_fixture);
2020-06-30 11:04:25 -05:00
}
#[track_caller]
pub(super) fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) {
2021-10-16 06:32:55 -05:00
let (analysis, file_id) = fixture::file(ra_fixture);
2022-03-18 12:11:16 -05:00
let mut expected = extract_annotations(&*analysis.file_text(file_id).unwrap());
2022-02-11 16:48:01 -06:00
let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
2022-03-18 12:11:16 -05:00
let actual = inlay_hints
.into_iter()
.map(|it| (it.range, it.label.to_string()))
.sorted_by_key(|(range, _)| range.start())
.collect::<Vec<_>>();
expected.sort_by_key(|(range, _)| range.start());
2020-07-16 14:51:44 -05:00
assert_eq!(expected, actual, "\nExpected:\n{:#?}\n\nActual:\n{:#?}", expected, actual);
2020-06-30 11:04:25 -05:00
}
2019-12-07 12:14:01 -06:00
#[track_caller]
pub(super) fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
2021-10-16 06:32:55 -05:00
let (analysis, file_id) = fixture::file(ra_fixture);
2022-02-11 16:48:01 -06:00
let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
expect.assert_debug_eq(&inlay_hints)
}
#[test]
2021-06-04 06:47:39 -05:00
fn hints_disabled() {
2020-06-30 11:04:25 -05:00
check_with_config(
InlayHintsConfig { render_colons: true, ..DISABLED_CONFIG },
r#"
2020-06-30 11:04:25 -05:00
fn foo(a: i32, b: i32) -> i32 { a + b }
2021-06-04 06:47:39 -05:00
fn main() {
let _x = foo(4, 4);
}"#,
);
}
2019-07-19 16:20:09 -05:00
}