Skip to main content

ide/
inlay_hints.rs

1use std::{
2    fmt::{self, Write},
3    mem::{self, take},
4};
5
6use either::Either;
7use hir::{
8    ClosureStyle, DisplayTarget, EditionedFileId, GenericParam, GenericParamId, HasVisibility,
9    HirDisplay, HirDisplayError, HirWrite, InRealFile, ModuleDef, ModuleDefId, Semantics, sym,
10};
11use ide_db::{
12    FileRange, RootDatabase, famous_defs::FamousDefs, ra_fixture::RaFixtureConfig,
13    text_edit::TextEditBuilder,
14};
15use ide_db::{FxHashSet, text_edit::TextEdit};
16use itertools::Itertools;
17use macros::UpmapFromRaFixture;
18use smallvec::{SmallVec, smallvec};
19use stdx::never;
20use syntax::{
21    SmolStr, SyntaxNode, TextRange, TextSize, WalkEvent,
22    ast::{self, AstNode, HasGenericParams},
23    format_smolstr, match_ast,
24};
25
26use crate::{FileId, navigation_target::TryToNav};
27
28mod adjustment;
29mod bind_pat;
30mod binding_mode;
31mod bounds;
32mod chaining;
33mod closing_brace;
34mod closure_captures;
35mod closure_ret;
36mod discriminant;
37mod extern_block;
38mod generic_param;
39mod implicit_drop;
40mod implicit_static;
41mod implied_dyn_trait;
42mod lifetime;
43mod param_name;
44mod placeholders;
45mod ra_fixture;
46mod range_exclusive;
47
48// Feature: Inlay Hints
49//
50// rust-analyzer shows additional information inline with the source code.
51// Editors usually render this using read-only virtual text snippets interspersed with code.
52//
53// rust-analyzer by default shows hints for
54//
55// * types of local variables
56// * names of function arguments
57// * names of const generic parameters
58// * types of chained expressions
59//
60// Optionally, one can enable additional hints for
61//
62// * return types of closure expressions
63// * elided lifetimes
64// * compiler inserted reborrows
65// * names of generic type and lifetime parameters
66//
67// Note: inlay hints for function argument names are heuristically omitted to reduce noise and will not appear if
68// any of the
69// [following criteria](https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L92-L99)
70// are met:
71//
72// * the parameter name is a suffix of the function's name
73// * the argument is a qualified constructing or call expression where the qualifier is an ADT
74// * exact argument<->parameter match(ignoring leading underscore) or parameter is a prefix/suffix
75//   of argument with _ splitting it off
76// * the parameter name starts with `ra_fixture`
77// * the parameter name is a
78// [well known name](https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L200)
79// in a unary function
80// * the parameter name is a
81// [single character](https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/crates/ide/src/inlay_hints/param_name.rs#L201)
82// in a unary function
83//
84// ![Inlay hints](https://user-images.githubusercontent.com/48062697/113020660-b5f98b80-917a-11eb-8d70-3be3fd558cdd.png)
85pub(crate) fn inlay_hints(
86    db: &RootDatabase,
87    file_id: FileId,
88    range_limit: Option<TextRange>,
89    config: &InlayHintsConfig<'_>,
90) -> Vec<InlayHint> {
91    let _p = tracing::info_span!("inlay_hints").entered();
92    let sema = Semantics::new(db);
93    let file_id = sema.attach_first_edition(file_id);
94    let file = sema.parse(file_id);
95    let file = file.syntax();
96
97    let mut acc = Vec::new();
98
99    let Some(scope) = sema.scope(file) else {
100        return acc;
101    };
102    let famous_defs = FamousDefs(&sema, scope.krate());
103    let display_target = famous_defs.1.to_display_target(sema.db);
104
105    let ctx = &mut InlayHintCtx::default();
106    let mut hints = |event| {
107        if let Some(node) = handle_event(ctx, event) {
108            hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
109        }
110    };
111    let mut preorder = file.preorder();
112    while let Some(event) = preorder.next() {
113        if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
114        {
115            preorder.skip_subtree();
116            continue;
117        }
118        hints(event);
119    }
120    if let Some(range_limit) = range_limit {
121        acc.retain(|hint| range_limit.contains_range(hint.range));
122    }
123    acc
124}
125
126#[derive(Default)]
127struct InlayHintCtx {
128    lifetime_stacks: Vec<Vec<SmolStr>>,
129    extern_block_parent: Option<ast::ExternBlock>,
130}
131
132pub(crate) fn inlay_hints_resolve(
133    db: &RootDatabase,
134    file_id: FileId,
135    resolve_range: TextRange,
136    hash: u64,
137    config: &InlayHintsConfig<'_>,
138    hasher: impl Fn(&InlayHint) -> u64,
139) -> Option<InlayHint> {
140    let _p = tracing::info_span!("inlay_hints_resolve").entered();
141    let sema = Semantics::new(db);
142    let file_id = sema.attach_first_edition(file_id);
143    let file = sema.parse(file_id);
144    let file = file.syntax();
145
146    let scope = sema.scope(file)?;
147    let famous_defs = FamousDefs(&sema, scope.krate());
148    let mut acc = Vec::new();
149
150    let display_target = famous_defs.1.to_display_target(sema.db);
151
152    let ctx = &mut InlayHintCtx::default();
153    let mut hints = |event| {
154        if let Some(node) = handle_event(ctx, event) {
155            hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
156        }
157    };
158
159    let mut preorder = file.preorder();
160    while let Some(event) = preorder.next() {
161        // FIXME: This can miss some hints that require the parent of the range to calculate
162        if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
163        {
164            preorder.skip_subtree();
165            continue;
166        }
167        hints(event);
168    }
169    acc.into_iter().find(|hint| hasher(hint) == hash)
170}
171
172fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
173    match node {
174        WalkEvent::Enter(node) => {
175            if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
176                let params = node
177                    .generic_param_list()
178                    .map(|it| {
179                        it.lifetime_params()
180                            .filter_map(|it| {
181                                it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
182                            })
183                            .collect()
184                    })
185                    .unwrap_or_default();
186                ctx.lifetime_stacks.push(params);
187            }
188            if let Some(node) = ast::ExternBlock::cast(node.clone()) {
189                ctx.extern_block_parent = Some(node);
190            }
191            Some(node)
192        }
193        WalkEvent::Leave(n) => {
194            if ast::AnyHasGenericParams::can_cast(n.kind()) {
195                ctx.lifetime_stacks.pop();
196            }
197            if ast::ExternBlock::can_cast(n.kind()) {
198                ctx.extern_block_parent = None;
199            }
200            None
201        }
202    }
203}
204
205// FIXME: At some point when our hir infra is fleshed out enough we should flip this and traverse the
206// HIR instead of the syntax tree.
207fn hints(
208    hints: &mut Vec<InlayHint>,
209    ctx: &mut InlayHintCtx,
210    famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
211    config: &InlayHintsConfig<'_>,
212    file_id: EditionedFileId,
213    display_target: DisplayTarget,
214    node: SyntaxNode,
215) {
216    closing_brace::hints(
217        hints,
218        sema,
219        config,
220        display_target,
221        InRealFile { file_id, value: node.clone() },
222    );
223    if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
224        generic_param::hints(hints, famous_defs, config, any_has_generic_args);
225    }
226
227    match_ast! {
228        match node {
229            ast::Expr(expr) => {
230                chaining::hints(hints, famous_defs, config, display_target, &expr);
231                adjustment::hints(hints, famous_defs, config, display_target, &expr);
232                match expr {
233                    ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
234                    ast::Expr::MethodCallExpr(it) => {
235                        param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
236                    }
237                    ast::Expr::ClosureExpr(it) => {
238                        closure_captures::hints(
239                            hints,
240                            famous_defs,
241                            config,
242                            Either::Left(it.clone()),
243                            file_id.edition(sema.db),
244                        );
245                        closure_ret::hints(hints, famous_defs, config, display_target, it)
246                    },
247                    ast::Expr::BlockExpr(it) => closure_captures::hints(
248                        hints,
249                        famous_defs,
250                        config,
251                        Either::Right(it),
252                        file_id.edition(sema.db),
253                    ),
254                    ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
255                    ast::Expr::Literal(it) => ra_fixture::hints(hints, famous_defs.0, file_id, config, it),
256                    _ => Some(()),
257                }
258            },
259            ast::Pat(it) => {
260                binding_mode::hints(hints, famous_defs, config, &it);
261                match it {
262                    ast::Pat::IdentPat(it) => {
263                        bind_pat::hints(hints, famous_defs, config, display_target, &it);
264                    }
265                    ast::Pat::RangePat(it) => {
266                        range_exclusive::hints(hints, famous_defs, config, it);
267                    }
268                    _ => {}
269                }
270                Some(())
271            },
272            ast::Item(it) => match it {
273                ast::Item::Fn(it) => {
274                    implicit_drop::hints(hints, famous_defs, config, display_target, &it);
275                    if let Some(extern_block) = &ctx.extern_block_parent {
276                        extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
277                    }
278                    lifetime::fn_hints(hints, ctx, famous_defs, config,  it)
279                },
280                ast::Item::Static(it) => {
281                    if let Some(extern_block) = &ctx.extern_block_parent {
282                        extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
283                    }
284                    implicit_static::hints(hints, famous_defs, config,  Either::Left(it))
285                },
286                ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
287                ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
288                ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
289                _ => None,
290            },
291            // FIXME: trait object type elisions
292            ast::Type(ty) => match ty {
293                ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config,  ptr),
294                ast::Type::PathType(path) => {
295                    lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
296                    implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
297                    Some(())
298                },
299                ast::Type::DynTraitType(dyn_) => {
300                    implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
301                    Some(())
302                },
303                ast::Type::InferType(placeholder) => {
304                    placeholders::type_hints(hints, famous_defs, config, display_target, placeholder);
305                    Some(())
306                },
307                _ => Some(()),
308            },
309            ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config,  it),
310            _ => Some(()),
311        }
312    };
313}
314
315#[derive(Clone, Debug)]
316pub struct InlayHintsConfig<'a> {
317    pub render_colons: bool,
318    pub type_hints: bool,
319    pub type_hints_placement: TypeHintsPlacement,
320    pub sized_bound: bool,
321    pub discriminant_hints: DiscriminantHints,
322    pub parameter_hints: bool,
323    pub parameter_hints_for_missing_arguments: bool,
324    pub generic_parameter_hints: GenericParameterHints,
325    pub chaining_hints: bool,
326    pub adjustment_hints: AdjustmentHints,
327    pub adjustment_hints_disable_reborrows: bool,
328    pub adjustment_hints_mode: AdjustmentHintsMode,
329    pub adjustment_hints_hide_outside_unsafe: bool,
330    pub closure_return_type_hints: ClosureReturnTypeHints,
331    pub closure_capture_hints: bool,
332    pub binding_mode_hints: bool,
333    pub implicit_drop_hints: bool,
334    pub implied_dyn_trait_hints: bool,
335    pub lifetime_elision_hints: LifetimeElisionHints,
336    pub param_names_for_lifetime_elision_hints: bool,
337    pub hide_inferred_type_hints: bool,
338    pub hide_named_constructor_hints: bool,
339    pub hide_closure_initialization_hints: bool,
340    pub hide_closure_parameter_hints: bool,
341    pub range_exclusive_hints: bool,
342    pub closure_style: ClosureStyle,
343    pub max_length: Option<usize>,
344    pub closing_brace_hints_min_lines: Option<usize>,
345    pub fields_to_resolve: InlayFieldsToResolve,
346    pub ra_fixture: RaFixtureConfig<'a>,
347}
348
349#[derive(Copy, Clone, Debug, PartialEq, Eq)]
350pub enum TypeHintsPlacement {
351    Inline,
352    EndOfLine,
353}
354
355impl InlayHintsConfig<'_> {
356    fn lazy_text_edit(&self, finish: impl FnOnce() -> TextEdit) -> LazyProperty<TextEdit> {
357        if self.fields_to_resolve.resolve_text_edits {
358            LazyProperty::Lazy
359        } else {
360            let edit = finish();
361            never!(edit.is_empty(), "inlay hint produced an empty text edit");
362            LazyProperty::Computed(edit)
363        }
364    }
365
366    fn lazy_tooltip(&self, finish: impl FnOnce() -> InlayTooltip) -> LazyProperty<InlayTooltip> {
367        if self.fields_to_resolve.resolve_hint_tooltip
368            && self.fields_to_resolve.resolve_label_tooltip
369        {
370            LazyProperty::Lazy
371        } else {
372            let tooltip = finish();
373            never!(
374                match &tooltip {
375                    InlayTooltip::String(s) => s,
376                    InlayTooltip::Markdown(s) => s,
377                }
378                .is_empty(),
379                "inlay hint produced an empty tooltip"
380            );
381            LazyProperty::Computed(tooltip)
382        }
383    }
384
385    /// This always reports a resolvable location, so only use this when it is very likely for a
386    /// location link to actually resolve but where computing `finish` would be costly.
387    fn lazy_location_opt(
388        &self,
389        finish: impl FnOnce() -> Option<FileRange>,
390    ) -> Option<LazyProperty<FileRange>> {
391        if self.fields_to_resolve.resolve_label_location {
392            Some(LazyProperty::Lazy)
393        } else {
394            finish().map(LazyProperty::Computed)
395        }
396    }
397}
398
399#[derive(Copy, Clone, Debug, PartialEq, Eq)]
400pub struct InlayFieldsToResolve {
401    pub resolve_text_edits: bool,
402    pub resolve_hint_tooltip: bool,
403    pub resolve_label_tooltip: bool,
404    pub resolve_label_location: bool,
405    pub resolve_label_command: bool,
406}
407
408impl InlayFieldsToResolve {
409    pub fn from_client_capabilities(client_capability_fields: &FxHashSet<&str>) -> Self {
410        Self {
411            resolve_text_edits: client_capability_fields.contains("textEdits"),
412            resolve_hint_tooltip: client_capability_fields.contains("tooltip"),
413            resolve_label_tooltip: client_capability_fields.contains("label.tooltip"),
414            resolve_label_location: client_capability_fields.contains("label.location"),
415            resolve_label_command: client_capability_fields.contains("label.command"),
416        }
417    }
418
419    pub const fn empty() -> Self {
420        Self {
421            resolve_text_edits: false,
422            resolve_hint_tooltip: false,
423            resolve_label_tooltip: false,
424            resolve_label_location: false,
425            resolve_label_command: false,
426        }
427    }
428}
429
430#[derive(Clone, Debug, PartialEq, Eq)]
431pub enum ClosureReturnTypeHints {
432    Always,
433    WithBlock,
434    Never,
435}
436
437#[derive(Clone, Debug, PartialEq, Eq)]
438pub enum DiscriminantHints {
439    Always,
440    Never,
441    Fieldless,
442}
443
444#[derive(Clone, Debug, PartialEq, Eq)]
445pub struct GenericParameterHints {
446    pub type_hints: bool,
447    pub lifetime_hints: bool,
448    pub const_hints: bool,
449}
450
451#[derive(Clone, Debug, PartialEq, Eq)]
452pub enum LifetimeElisionHints {
453    Always,
454    SkipTrivial,
455    Never,
456}
457
458#[derive(Clone, Debug, PartialEq, Eq)]
459pub enum AdjustmentHints {
460    Always,
461    BorrowsOnly,
462    Never,
463}
464
465#[derive(Copy, Clone, Debug, PartialEq, Eq)]
466pub enum AdjustmentHintsMode {
467    Prefix,
468    Postfix,
469    PreferPrefix,
470    PreferPostfix,
471}
472
473#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
474pub enum InlayKind {
475    Adjustment,
476    BindingMode,
477    Chaining,
478    ClosingBrace,
479    ClosureCapture,
480    Discriminant,
481    GenericParamList,
482    Lifetime,
483    Parameter,
484    GenericParameter,
485    Type,
486    Dyn,
487    Drop,
488    RangeExclusive,
489    ExternUnsafety,
490}
491
492#[derive(Debug, Hash)]
493pub enum InlayHintPosition {
494    Before,
495    After,
496}
497
498#[derive(Debug, UpmapFromRaFixture)]
499pub struct InlayHint {
500    /// The text range this inlay hint applies to.
501    pub range: TextRange,
502    pub position: InlayHintPosition,
503    pub pad_left: bool,
504    pub pad_right: bool,
505    /// The kind of this inlay hint.
506    pub kind: InlayKind,
507    /// The actual label to show in the inlay hint.
508    pub label: InlayHintLabel,
509    /// Text edit to apply when "accepting" this inlay hint.
510    pub text_edit: Option<LazyProperty<TextEdit>>,
511    /// Range to recompute inlay hints when trying to resolve for this hint. If this is none, the
512    /// hint does not support resolving.
513    pub resolve_parent: Option<TextRange>,
514}
515
516/// A type signaling that a value is either computed, or is available for computation.
517#[derive(Clone, Debug, Default, UpmapFromRaFixture)]
518pub enum LazyProperty<T> {
519    Computed(T),
520    #[default]
521    Lazy,
522}
523
524impl<T> LazyProperty<T> {
525    pub fn computed(self) -> Option<T> {
526        match self {
527            LazyProperty::Computed(it) => Some(it),
528            _ => None,
529        }
530    }
531
532    pub fn is_lazy(&self) -> bool {
533        matches!(self, Self::Lazy)
534    }
535}
536
537impl std::hash::Hash for InlayHint {
538    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
539        self.range.hash(state);
540        self.position.hash(state);
541        self.pad_left.hash(state);
542        self.pad_right.hash(state);
543        self.kind.hash(state);
544        self.label.hash(state);
545        mem::discriminant(&self.text_edit).hash(state);
546    }
547}
548
549impl InlayHint {
550    fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint {
551        InlayHint {
552            range,
553            kind,
554            label: InlayHintLabel::from(")"),
555            text_edit: None,
556            position: InlayHintPosition::After,
557            pad_left: false,
558            pad_right: false,
559            resolve_parent: None,
560        }
561    }
562}
563
564#[derive(Debug, Hash)]
565pub enum InlayTooltip {
566    String(String),
567    Markdown(String),
568}
569
570#[derive(Default, Hash, UpmapFromRaFixture)]
571pub struct InlayHintLabel {
572    pub parts: SmallVec<[InlayHintLabelPart; 1]>,
573}
574
575impl InlayHintLabel {
576    pub fn simple(
577        s: impl Into<String>,
578        tooltip: Option<LazyProperty<InlayTooltip>>,
579        linked_location: Option<LazyProperty<FileRange>>,
580    ) -> InlayHintLabel {
581        InlayHintLabel {
582            parts: smallvec![InlayHintLabelPart { text: s.into(), linked_location, tooltip }],
583        }
584    }
585
586    pub fn prepend_str(&mut self, s: &str) {
587        match &mut *self.parts {
588            [InlayHintLabelPart { text, linked_location: None, tooltip: None }, ..] => {
589                text.insert_str(0, s)
590            }
591            _ => self.parts.insert(
592                0,
593                InlayHintLabelPart { text: s.into(), linked_location: None, tooltip: None },
594            ),
595        }
596    }
597
598    pub fn append_str(&mut self, s: &str) {
599        match &mut *self.parts {
600            [.., InlayHintLabelPart { text, linked_location: None, tooltip: None }] => {
601                text.push_str(s)
602            }
603            _ => self.parts.push(InlayHintLabelPart {
604                text: s.into(),
605                linked_location: None,
606                tooltip: None,
607            }),
608        }
609    }
610
611    pub fn append_part(&mut self, part: InlayHintLabelPart) {
612        if part.linked_location.is_none()
613            && part.tooltip.is_none()
614            && let Some(InlayHintLabelPart { text, linked_location: None, tooltip: None }) =
615                self.parts.last_mut()
616        {
617            text.push_str(&part.text);
618            return;
619        }
620        self.parts.push(part);
621    }
622}
623
624impl From<String> for InlayHintLabel {
625    fn from(s: String) -> Self {
626        Self {
627            parts: smallvec![InlayHintLabelPart { text: s, linked_location: None, tooltip: None }],
628        }
629    }
630}
631
632impl From<&str> for InlayHintLabel {
633    fn from(s: &str) -> Self {
634        Self {
635            parts: smallvec![InlayHintLabelPart {
636                text: s.into(),
637                linked_location: None,
638                tooltip: None
639            }],
640        }
641    }
642}
643
644impl fmt::Display for InlayHintLabel {
645    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
646        write!(f, "{}", self.parts.iter().map(|part| &part.text).format(""))
647    }
648}
649
650impl fmt::Debug for InlayHintLabel {
651    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
652        f.debug_list().entries(&self.parts).finish()
653    }
654}
655
656#[derive(UpmapFromRaFixture)]
657pub struct InlayHintLabelPart {
658    pub text: String,
659    /// Source location represented by this label part. The client will use this to fetch the part's
660    /// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location
661    /// refers to (not necessarily the location itself).
662    /// When setting this, no tooltip must be set on the containing hint, or VS Code will display
663    /// them both.
664    pub linked_location: Option<LazyProperty<FileRange>>,
665    /// The tooltip to show when hovering over the inlay hint, this may invoke other actions like
666    /// hover requests to show.
667    pub tooltip: Option<LazyProperty<InlayTooltip>>,
668}
669
670impl std::hash::Hash for InlayHintLabelPart {
671    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
672        self.text.hash(state);
673        self.linked_location.is_some().hash(state);
674        self.tooltip.is_some().hash(state);
675    }
676}
677
678impl fmt::Debug for InlayHintLabelPart {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        match self {
681            Self { text, linked_location: None, tooltip: None | Some(LazyProperty::Lazy) } => {
682                text.fmt(f)
683            }
684            Self { text, linked_location, tooltip } => f
685                .debug_struct("InlayHintLabelPart")
686                .field("text", text)
687                .field("linked_location", linked_location)
688                .field(
689                    "tooltip",
690                    &tooltip.as_ref().map_or("", |it| match it {
691                        LazyProperty::Computed(
692                            InlayTooltip::String(it) | InlayTooltip::Markdown(it),
693                        ) => it,
694                        LazyProperty::Lazy => "",
695                    }),
696                )
697                .finish(),
698        }
699    }
700}
701
702#[derive(Debug)]
703struct InlayHintLabelBuilder<'a, 'db> {
704    sema: &'a Semantics<'db, RootDatabase>,
705    result: InlayHintLabel,
706    last_part: String,
707    resolve: bool,
708    location: Option<LazyProperty<FileRange>>,
709}
710
711impl fmt::Write for InlayHintLabelBuilder<'_, '_> {
712    fn write_str(&mut self, s: &str) -> fmt::Result {
713        self.last_part.write_str(s)
714    }
715}
716
717impl HirWrite for InlayHintLabelBuilder<'_, '_> {
718    fn start_location_link(&mut self, def: ModuleDefId) {
719        never!(self.location.is_some(), "location link is already started");
720        self.make_new_part();
721
722        self.location = Some(if self.resolve {
723            LazyProperty::Lazy
724        } else {
725            LazyProperty::Computed({
726                let Some(location) = ModuleDef::from(def).try_to_nav(self.sema) else { return };
727                let location = location.call_site();
728                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
729            })
730        });
731    }
732
733    fn start_location_link_generic(&mut self, def: GenericParamId) {
734        never!(self.location.is_some(), "location link is already started");
735        self.make_new_part();
736
737        self.location = Some(if self.resolve {
738            LazyProperty::Lazy
739        } else {
740            LazyProperty::Computed({
741                let Some(location) = GenericParam::from(def).try_to_nav(self.sema) else { return };
742                let location = location.call_site();
743                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
744            })
745        });
746    }
747
748    fn end_location_link(&mut self) {
749        self.make_new_part();
750    }
751}
752
753impl InlayHintLabelBuilder<'_, '_> {
754    fn make_new_part(&mut self) {
755        let text = take(&mut self.last_part);
756        if !text.is_empty() {
757            self.result.parts.push(InlayHintLabelPart {
758                text,
759                linked_location: self.location.take(),
760                tooltip: None,
761            });
762        }
763    }
764
765    fn finish(mut self) -> InlayHintLabel {
766        self.make_new_part();
767        self.result
768    }
769}
770
771fn label_of_ty<'db>(
772    famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, 'db>,
773    config: &InlayHintsConfig<'_>,
774    ty: &hir::Type<'db>,
775    display_target: DisplayTarget,
776) -> Option<InlayHintLabel> {
777    fn rec<'db>(
778        sema: &Semantics<'db, RootDatabase>,
779        famous_defs: &FamousDefs<'_, 'db>,
780        mut max_length: Option<usize>,
781        ty: &hir::Type<'db>,
782        label_builder: &mut InlayHintLabelBuilder<'_, '_>,
783        config: &InlayHintsConfig<'_>,
784        display_target: DisplayTarget,
785    ) -> Result<(), HirDisplayError> {
786        let iter_item_type = hint_iterator(sema, famous_defs, ty);
787        match iter_item_type {
788            Some((iter_trait, item, ty)) => {
789                const LABEL_START: &str = "impl ";
790                const LABEL_ITERATOR: &str = "Iterator";
791                const LABEL_MIDDLE: &str = "<";
792                const LABEL_ITEM: &str = "Item";
793                const LABEL_MIDDLE2: &str = " = ";
794                const LABEL_END: &str = ">";
795
796                max_length = max_length.map(|len| {
797                    len.saturating_sub(
798                        LABEL_START.len()
799                            + LABEL_ITERATOR.len()
800                            + LABEL_MIDDLE.len()
801                            + LABEL_MIDDLE2.len()
802                            + LABEL_END.len(),
803                    )
804                });
805
806                let module_def_location = |label_builder: &mut InlayHintLabelBuilder<'_, '_>,
807                                           def: ModuleDef,
808                                           name| {
809                    let def = def.try_into();
810                    if let Ok(def) = def {
811                        label_builder.start_location_link(def);
812                    }
813                    #[expect(
814                        clippy::question_mark,
815                        reason = "false positive; replacing with `?` leads to 'type annotations needed' error"
816                    )]
817                    if let Err(err) = label_builder.write_str(name) {
818                        return Err(err);
819                    }
820                    if def.is_ok() {
821                        label_builder.end_location_link();
822                    }
823                    Ok(())
824                };
825
826                label_builder.write_str(LABEL_START)?;
827                module_def_location(label_builder, ModuleDef::from(iter_trait), LABEL_ITERATOR)?;
828                label_builder.write_str(LABEL_MIDDLE)?;
829                module_def_location(label_builder, ModuleDef::from(item), LABEL_ITEM)?;
830                label_builder.write_str(LABEL_MIDDLE2)?;
831                rec(sema, famous_defs, max_length, &ty, label_builder, config, display_target)?;
832                label_builder.write_str(LABEL_END)?;
833                Ok(())
834            }
835            None => ty
836                .display_truncated(sema.db, max_length, display_target)
837                .with_closure_style(config.closure_style)
838                .write_to(label_builder),
839        }
840    }
841
842    let mut label_builder = InlayHintLabelBuilder {
843        sema,
844        last_part: String::new(),
845        location: None,
846        result: InlayHintLabel::default(),
847        resolve: config.fields_to_resolve.resolve_label_location,
848    };
849    let _ =
850        rec(sema, famous_defs, config.max_length, ty, &mut label_builder, config, display_target);
851    let r = label_builder.finish();
852    Some(r)
853}
854
855/// Checks if the type is an Iterator from std::iter and returns the iterator trait and the item type of the concrete iterator.
856fn hint_iterator<'db>(
857    sema: &Semantics<'db, RootDatabase>,
858    famous_defs: &FamousDefs<'_, 'db>,
859    ty: &hir::Type<'db>,
860) -> Option<(hir::Trait, hir::TypeAlias, hir::Type<'db>)> {
861    let db = sema.db;
862    let strukt = ty.strip_references().as_adt()?;
863    let krate = strukt.module(db).krate(db);
864    if krate != famous_defs.core()? {
865        return None;
866    }
867    let iter_trait = famous_defs.core_iter_Iterator()?;
868    let iter_mod = famous_defs.core_iter()?;
869
870    // Assert that this struct comes from `core::iter`.
871    if !(strukt.visibility(db) == hir::Visibility::Public
872        && strukt.module(db).path_to_root(db).contains(&iter_mod))
873    {
874        return None;
875    }
876
877    if ty.impls_trait(db, iter_trait, &[]) {
878        let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item {
879            hir::AssocItem::TypeAlias(alias) if alias.name(db) == sym::Item => Some(alias),
880            _ => None,
881        })?;
882        if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) {
883            return Some((iter_trait, assoc_type_item, ty));
884        }
885    }
886
887    None
888}
889
890fn ty_to_text_edit(
891    sema: &Semantics<'_, RootDatabase>,
892    config: &InlayHintsConfig<'_>,
893    node_for_hint: &SyntaxNode,
894    ty: &hir::Type<'_>,
895    offset_to_insert_ty: TextSize,
896    additional_edits: &dyn Fn(&mut TextEditBuilder),
897    prefix: impl Into<String>,
898) -> Option<LazyProperty<TextEdit>> {
899    // FIXME: Limit the length and bail out on excess somehow?
900    let rendered = sema
901        .scope(node_for_hint)
902        .and_then(|scope| ty.display_source_code(scope.db, scope.module().into(), false).ok())?;
903    Some(config.lazy_text_edit(|| {
904        let mut builder = TextEdit::builder();
905        builder.insert(offset_to_insert_ty, prefix.into());
906        builder.insert(offset_to_insert_ty, rendered);
907
908        additional_edits(&mut builder);
909
910        builder.finish()
911    }))
912}
913
914fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
915    matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
916}
917
918#[cfg(test)]
919mod tests {
920
921    use expect_test::Expect;
922    use hir::ClosureStyle;
923    use ide_db::ra_fixture::RaFixtureConfig;
924    use itertools::Itertools;
925    use test_utils::extract_annotations;
926
927    use crate::DiscriminantHints;
928    use crate::inlay_hints::{AdjustmentHints, AdjustmentHintsMode};
929    use crate::{LifetimeElisionHints, fixture, inlay_hints::InlayHintsConfig};
930
931    use super::{
932        ClosureReturnTypeHints, GenericParameterHints, InlayFieldsToResolve, TypeHintsPlacement,
933    };
934
935    pub(super) const DISABLED_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
936        discriminant_hints: DiscriminantHints::Never,
937        render_colons: false,
938        type_hints: false,
939        type_hints_placement: TypeHintsPlacement::Inline,
940        parameter_hints: false,
941        parameter_hints_for_missing_arguments: false,
942        sized_bound: false,
943        generic_parameter_hints: GenericParameterHints {
944            type_hints: false,
945            lifetime_hints: false,
946            const_hints: false,
947        },
948        chaining_hints: false,
949        lifetime_elision_hints: LifetimeElisionHints::Never,
950        closure_return_type_hints: ClosureReturnTypeHints::Never,
951        closure_capture_hints: false,
952        adjustment_hints: AdjustmentHints::Never,
953        adjustment_hints_disable_reborrows: false,
954        adjustment_hints_mode: AdjustmentHintsMode::Prefix,
955        adjustment_hints_hide_outside_unsafe: false,
956        binding_mode_hints: false,
957        hide_inferred_type_hints: false,
958        hide_named_constructor_hints: false,
959        hide_closure_initialization_hints: false,
960        hide_closure_parameter_hints: false,
961        closure_style: ClosureStyle::ImplFn,
962        param_names_for_lifetime_elision_hints: false,
963        max_length: None,
964        closing_brace_hints_min_lines: None,
965        fields_to_resolve: InlayFieldsToResolve::empty(),
966        implicit_drop_hints: false,
967        implied_dyn_trait_hints: false,
968        range_exclusive_hints: false,
969        ra_fixture: RaFixtureConfig::default(),
970    };
971    pub(super) const TEST_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
972        type_hints: true,
973        type_hints_placement: TypeHintsPlacement::Inline,
974        parameter_hints: true,
975        chaining_hints: true,
976        closure_return_type_hints: ClosureReturnTypeHints::WithBlock,
977        binding_mode_hints: true,
978        lifetime_elision_hints: LifetimeElisionHints::Always,
979        ..DISABLED_CONFIG
980    };
981
982    #[track_caller]
983    pub(super) fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
984        check_with_config(TEST_CONFIG, ra_fixture);
985    }
986
987    #[track_caller]
988    pub(super) fn check_with_config(
989        config: InlayHintsConfig<'_>,
990        #[rust_analyzer::rust_fixture] ra_fixture: &str,
991    ) {
992        let (analysis, file_id) = fixture::file(ra_fixture);
993        let mut expected = extract_annotations(&analysis.file_text(file_id).unwrap());
994        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
995        let actual = inlay_hints
996            .into_iter()
997            // FIXME: We trim the start because some inlay produces leading whitespace which is not properly supported by our annotation extraction
998            .map(|it| (it.range, it.label.to_string().trim_start().to_owned()))
999            .sorted_by_key(|(range, _)| range.start())
1000            .collect::<Vec<_>>();
1001        expected.sort_by_key(|(range, _)| range.start());
1002
1003        assert_eq!(expected, actual, "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}");
1004    }
1005
1006    #[track_caller]
1007    pub(super) fn check_expect(
1008        config: InlayHintsConfig<'_>,
1009        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1010        expect: Expect,
1011    ) {
1012        let (analysis, file_id) = fixture::file(ra_fixture);
1013        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1014        let filtered =
1015            inlay_hints.into_iter().map(|hint| (hint.range, hint.label)).collect::<Vec<_>>();
1016        expect.assert_debug_eq(&filtered)
1017    }
1018
1019    /// Computes inlay hints for the fixture, applies all the provided text edits and then runs
1020    /// expect test.
1021    #[track_caller]
1022    pub(super) fn check_edit(
1023        config: InlayHintsConfig<'_>,
1024        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1025        expect: Expect,
1026    ) {
1027        let (analysis, file_id) = fixture::file(ra_fixture);
1028        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1029
1030        let edits = inlay_hints
1031            .into_iter()
1032            .filter_map(|hint| hint.text_edit?.computed())
1033            .reduce(|mut acc, next| {
1034                acc.union(next).expect("merging text edits failed");
1035                acc
1036            })
1037            .expect("no edit returned");
1038
1039        let mut actual = analysis.file_text(file_id).unwrap().to_string();
1040        edits.apply(&mut actual);
1041        expect.assert_eq(&actual);
1042    }
1043
1044    #[track_caller]
1045    pub(super) fn check_no_edit(
1046        config: InlayHintsConfig<'_>,
1047        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1048    ) {
1049        let (analysis, file_id) = fixture::file(ra_fixture);
1050        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1051
1052        let edits: Vec<_> =
1053            inlay_hints.into_iter().filter_map(|hint| hint.text_edit?.computed()).collect();
1054
1055        assert!(edits.is_empty(), "unexpected edits: {edits:?}");
1056    }
1057
1058    #[test]
1059    fn hints_disabled() {
1060        check_with_config(
1061            InlayHintsConfig { render_colons: true, ..DISABLED_CONFIG },
1062            r#"
1063fn foo(a: i32, b: i32) -> i32 { a + b }
1064fn main() {
1065    let _x = foo(4, 4);
1066}"#,
1067        );
1068    }
1069
1070    #[test]
1071    fn regression_18840() {
1072        check(
1073            r#"
1074//- proc_macros: issue_18840
1075#[proc_macros::issue_18840]
1076fn foo() {
1077    let
1078    loop {}
1079}
1080"#,
1081        );
1082    }
1083
1084    #[test]
1085    fn regression_18898() {
1086        check(
1087            r#"
1088//- proc_macros: issue_18898
1089#[proc_macros::issue_18898]
1090fn foo() {
1091    let
1092}
1093"#,
1094        );
1095    }
1096
1097    #[test]
1098    fn closure_dependency_cycle_no_panic() {
1099        check(
1100            r#"
1101//- minicore: fn
1102fn foo() {
1103    let closure;
1104     // ^^^^^^^ impl FnOnce()
1105    closure = || {
1106        closure();
1107    };
1108}
1109
1110fn bar() {
1111    let closure1;
1112     // ^^^^^^^^ impl FnOnce()
1113    let closure2;
1114     // ^^^^^^^^ impl FnOnce()
1115    closure1 = || {
1116        closure2();
1117    };
1118    closure2 = || {
1119        closure1();
1120    };
1121}
1122        "#,
1123        );
1124    }
1125
1126    #[test]
1127    fn regression_19610() {
1128        check(
1129            r#"
1130trait Trait {
1131    type Assoc;
1132}
1133struct Foo<A>(A);
1134impl<A: Trait<Assoc = impl Trait>> Foo<A> {
1135    fn foo<'a, 'b>(_: &'a [i32], _: &'b [i32]) {}
1136}
1137
1138fn bar() {
1139    Foo::foo(&[1], &[2]);
1140}
1141"#,
1142        );
1143    }
1144
1145    #[test]
1146    fn regression_20239() {
1147        check_with_config(
1148            InlayHintsConfig { parameter_hints: true, type_hints: true, ..DISABLED_CONFIG },
1149            r#"
1150//- minicore: fn
1151trait Iterator {
1152    type Item;
1153    fn map<B, F: FnMut(Self::Item) -> B>(self, f: F);
1154}
1155trait ToString {
1156    fn to_string(&self);
1157}
1158
1159fn check_tostr_eq<L, R>(left: L, right: R)
1160where
1161    L: Iterator,
1162    L::Item: ToString,
1163    R: Iterator,
1164    R::Item: ToString,
1165{
1166    left.map(|s| s.to_string());
1167           // ^ impl ToString
1168    right.map(|s| s.to_string());
1169            // ^ impl ToString
1170}
1171        "#,
1172        );
1173    }
1174}