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: ImplicitDropHints,
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 ImplicitDropHints {
460    Always,
461    WithDropGlue,
462    Never,
463}
464
465#[derive(Clone, Debug, PartialEq, Eq)]
466pub enum AdjustmentHints {
467    Always,
468    BorrowsOnly,
469    Never,
470}
471
472#[derive(Copy, Clone, Debug, PartialEq, Eq)]
473pub enum AdjustmentHintsMode {
474    Prefix,
475    Postfix,
476    PreferPrefix,
477    PreferPostfix,
478}
479
480#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
481pub enum InlayKind {
482    Adjustment,
483    BindingMode,
484    Chaining,
485    ClosingBrace,
486    ClosureCapture,
487    Discriminant,
488    GenericParamList,
489    Lifetime,
490    Parameter,
491    GenericParameter,
492    Type,
493    Dyn,
494    Drop,
495    RangeExclusive,
496    ExternUnsafety,
497}
498
499#[derive(Debug, Hash)]
500pub enum InlayHintPosition {
501    Before,
502    After,
503}
504
505#[derive(Debug, UpmapFromRaFixture)]
506pub struct InlayHint {
507    /// The text range this inlay hint applies to.
508    pub range: TextRange,
509    pub position: InlayHintPosition,
510    pub pad_left: bool,
511    pub pad_right: bool,
512    /// The kind of this inlay hint.
513    pub kind: InlayKind,
514    /// The actual label to show in the inlay hint.
515    pub label: InlayHintLabel,
516    /// Text edit to apply when "accepting" this inlay hint.
517    pub text_edit: Option<LazyProperty<TextEdit>>,
518    /// Range to recompute inlay hints when trying to resolve for this hint. If this is none, the
519    /// hint does not support resolving.
520    pub resolve_parent: Option<TextRange>,
521}
522
523/// A type signaling that a value is either computed, or is available for computation.
524#[derive(Clone, Debug, Default, UpmapFromRaFixture)]
525pub enum LazyProperty<T> {
526    Computed(T),
527    #[default]
528    Lazy,
529}
530
531impl<T> LazyProperty<T> {
532    pub fn computed(self) -> Option<T> {
533        match self {
534            LazyProperty::Computed(it) => Some(it),
535            _ => None,
536        }
537    }
538
539    pub fn is_lazy(&self) -> bool {
540        matches!(self, Self::Lazy)
541    }
542}
543
544impl std::hash::Hash for InlayHint {
545    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
546        self.range.hash(state);
547        self.position.hash(state);
548        self.pad_left.hash(state);
549        self.pad_right.hash(state);
550        self.kind.hash(state);
551        self.label.hash(state);
552        mem::discriminant(&self.text_edit).hash(state);
553    }
554}
555
556impl InlayHint {
557    fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint {
558        InlayHint {
559            range,
560            kind,
561            label: InlayHintLabel::from(")"),
562            text_edit: None,
563            position: InlayHintPosition::After,
564            pad_left: false,
565            pad_right: false,
566            resolve_parent: None,
567        }
568    }
569}
570
571#[derive(Debug, Hash)]
572pub enum InlayTooltip {
573    String(String),
574    Markdown(String),
575}
576
577#[derive(Default, Hash, UpmapFromRaFixture)]
578pub struct InlayHintLabel {
579    pub parts: SmallVec<[InlayHintLabelPart; 1]>,
580}
581
582impl InlayHintLabel {
583    pub fn simple(
584        s: impl Into<String>,
585        tooltip: Option<LazyProperty<InlayTooltip>>,
586        linked_location: Option<LazyProperty<FileRange>>,
587    ) -> InlayHintLabel {
588        InlayHintLabel {
589            parts: smallvec![InlayHintLabelPart { text: s.into(), linked_location, tooltip }],
590        }
591    }
592
593    pub fn prepend_str(&mut self, s: &str) {
594        match &mut *self.parts {
595            [InlayHintLabelPart { text, linked_location: None, tooltip: None }, ..] => {
596                text.insert_str(0, s)
597            }
598            _ => self.parts.insert(
599                0,
600                InlayHintLabelPart { text: s.into(), linked_location: None, tooltip: None },
601            ),
602        }
603    }
604
605    pub fn append_str(&mut self, s: &str) {
606        match &mut *self.parts {
607            [.., InlayHintLabelPart { text, linked_location: None, tooltip: None }] => {
608                text.push_str(s)
609            }
610            _ => self.parts.push(InlayHintLabelPart {
611                text: s.into(),
612                linked_location: None,
613                tooltip: None,
614            }),
615        }
616    }
617
618    pub fn append_part(&mut self, part: InlayHintLabelPart) {
619        if part.linked_location.is_none()
620            && part.tooltip.is_none()
621            && let Some(InlayHintLabelPart { text, linked_location: None, tooltip: None }) =
622                self.parts.last_mut()
623        {
624            text.push_str(&part.text);
625            return;
626        }
627        self.parts.push(part);
628    }
629}
630
631impl From<String> for InlayHintLabel {
632    fn from(s: String) -> Self {
633        Self {
634            parts: smallvec![InlayHintLabelPart { text: s, linked_location: None, tooltip: None }],
635        }
636    }
637}
638
639impl From<&str> for InlayHintLabel {
640    fn from(s: &str) -> Self {
641        Self {
642            parts: smallvec![InlayHintLabelPart {
643                text: s.into(),
644                linked_location: None,
645                tooltip: None
646            }],
647        }
648    }
649}
650
651impl fmt::Display for InlayHintLabel {
652    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653        write!(f, "{}", self.parts.iter().map(|part| &part.text).format(""))
654    }
655}
656
657impl fmt::Debug for InlayHintLabel {
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        f.debug_list().entries(&self.parts).finish()
660    }
661}
662
663#[derive(UpmapFromRaFixture)]
664pub struct InlayHintLabelPart {
665    pub text: String,
666    /// Source location represented by this label part. The client will use this to fetch the part's
667    /// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location
668    /// refers to (not necessarily the location itself).
669    /// When setting this, no tooltip must be set on the containing hint, or VS Code will display
670    /// them both.
671    pub linked_location: Option<LazyProperty<FileRange>>,
672    /// The tooltip to show when hovering over the inlay hint, this may invoke other actions like
673    /// hover requests to show.
674    pub tooltip: Option<LazyProperty<InlayTooltip>>,
675}
676
677impl std::hash::Hash for InlayHintLabelPart {
678    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
679        self.text.hash(state);
680        self.linked_location.is_some().hash(state);
681        self.tooltip.is_some().hash(state);
682    }
683}
684
685impl fmt::Debug for InlayHintLabelPart {
686    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
687        match self {
688            Self { text, linked_location: None, tooltip: None | Some(LazyProperty::Lazy) } => {
689                text.fmt(f)
690            }
691            Self { text, linked_location, tooltip } => f
692                .debug_struct("InlayHintLabelPart")
693                .field("text", text)
694                .field("linked_location", linked_location)
695                .field(
696                    "tooltip",
697                    &tooltip.as_ref().map_or("", |it| match it {
698                        LazyProperty::Computed(
699                            InlayTooltip::String(it) | InlayTooltip::Markdown(it),
700                        ) => it,
701                        LazyProperty::Lazy => "",
702                    }),
703                )
704                .finish(),
705        }
706    }
707}
708
709#[derive(Debug)]
710struct InlayHintLabelBuilder<'a, 'db> {
711    sema: &'a Semantics<'db, RootDatabase>,
712    result: InlayHintLabel,
713    last_part: String,
714    resolve: bool,
715    location: Option<LazyProperty<FileRange>>,
716}
717
718impl fmt::Write for InlayHintLabelBuilder<'_, '_> {
719    fn write_str(&mut self, s: &str) -> fmt::Result {
720        self.last_part.write_str(s)
721    }
722}
723
724impl HirWrite for InlayHintLabelBuilder<'_, '_> {
725    fn start_location_link(&mut self, def: ModuleDefId) {
726        never!(self.location.is_some(), "location link is already started");
727        self.make_new_part();
728
729        self.location = Some(if self.resolve {
730            LazyProperty::Lazy
731        } else {
732            LazyProperty::Computed({
733                let Some(location) = ModuleDef::from(def).try_to_nav(self.sema) else { return };
734                let location = location.call_site();
735                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
736            })
737        });
738    }
739
740    fn start_location_link_generic(&mut self, def: GenericParamId) {
741        never!(self.location.is_some(), "location link is already started");
742        self.make_new_part();
743
744        self.location = Some(if self.resolve {
745            LazyProperty::Lazy
746        } else {
747            LazyProperty::Computed({
748                let Some(location) = GenericParam::from(def).try_to_nav(self.sema) else { return };
749                let location = location.call_site();
750                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
751            })
752        });
753    }
754
755    fn end_location_link(&mut self) {
756        self.make_new_part();
757    }
758}
759
760impl InlayHintLabelBuilder<'_, '_> {
761    fn make_new_part(&mut self) {
762        let text = take(&mut self.last_part);
763        if !text.is_empty() {
764            self.result.parts.push(InlayHintLabelPart {
765                text,
766                linked_location: self.location.take(),
767                tooltip: None,
768            });
769        }
770    }
771
772    fn finish(mut self) -> InlayHintLabel {
773        self.make_new_part();
774        self.result
775    }
776}
777
778fn label_of_ty<'db>(
779    famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, 'db>,
780    config: &InlayHintsConfig<'_>,
781    ty: &hir::Type<'db>,
782    display_target: DisplayTarget,
783) -> Option<InlayHintLabel> {
784    fn rec<'db>(
785        sema: &Semantics<'db, RootDatabase>,
786        famous_defs: &FamousDefs<'_, 'db>,
787        mut max_length: Option<usize>,
788        ty: &hir::Type<'db>,
789        label_builder: &mut InlayHintLabelBuilder<'_, '_>,
790        config: &InlayHintsConfig<'_>,
791        display_target: DisplayTarget,
792    ) -> Result<(), HirDisplayError> {
793        let iter_item_type = hint_iterator(sema, famous_defs, ty);
794        match iter_item_type {
795            Some((iter_trait, item, ty)) => {
796                const LABEL_START: &str = "impl ";
797                const LABEL_ITERATOR: &str = "Iterator";
798                const LABEL_MIDDLE: &str = "<";
799                const LABEL_ITEM: &str = "Item";
800                const LABEL_MIDDLE2: &str = " = ";
801                const LABEL_END: &str = ">";
802
803                max_length = max_length.map(|len| {
804                    len.saturating_sub(
805                        LABEL_START.len()
806                            + LABEL_ITERATOR.len()
807                            + LABEL_MIDDLE.len()
808                            + LABEL_MIDDLE2.len()
809                            + LABEL_END.len(),
810                    )
811                });
812
813                let module_def_location = |label_builder: &mut InlayHintLabelBuilder<'_, '_>,
814                                           def: ModuleDef,
815                                           name| {
816                    let def = def.try_into();
817                    if let Ok(def) = def {
818                        label_builder.start_location_link(def);
819                    }
820                    #[expect(
821                        clippy::question_mark,
822                        reason = "false positive; replacing with `?` leads to 'type annotations needed' error"
823                    )]
824                    if let Err(err) = label_builder.write_str(name) {
825                        return Err(err);
826                    }
827                    if def.is_ok() {
828                        label_builder.end_location_link();
829                    }
830                    Ok(())
831                };
832
833                label_builder.write_str(LABEL_START)?;
834                module_def_location(label_builder, ModuleDef::from(iter_trait), LABEL_ITERATOR)?;
835                label_builder.write_str(LABEL_MIDDLE)?;
836                module_def_location(label_builder, ModuleDef::from(item), LABEL_ITEM)?;
837                label_builder.write_str(LABEL_MIDDLE2)?;
838                rec(sema, famous_defs, max_length, &ty, label_builder, config, display_target)?;
839                label_builder.write_str(LABEL_END)?;
840                Ok(())
841            }
842            None => ty
843                .display_truncated(sema.db, max_length, display_target)
844                .with_closure_style(config.closure_style)
845                .write_to(label_builder),
846        }
847    }
848
849    let mut label_builder = InlayHintLabelBuilder {
850        sema,
851        last_part: String::new(),
852        location: None,
853        result: InlayHintLabel::default(),
854        resolve: config.fields_to_resolve.resolve_label_location,
855    };
856    let _ =
857        rec(sema, famous_defs, config.max_length, ty, &mut label_builder, config, display_target);
858    let r = label_builder.finish();
859    Some(r)
860}
861
862/// Checks if the type is an Iterator from std::iter and returns the iterator trait and the item type of the concrete iterator.
863fn hint_iterator<'db>(
864    sema: &Semantics<'db, RootDatabase>,
865    famous_defs: &FamousDefs<'_, 'db>,
866    ty: &hir::Type<'db>,
867) -> Option<(hir::Trait, hir::TypeAlias, hir::Type<'db>)> {
868    let db = sema.db;
869    let strukt = ty.strip_references().as_adt()?;
870    let krate = strukt.module(db).krate(db);
871    if krate != famous_defs.core()? {
872        return None;
873    }
874    let iter_trait = famous_defs.core_iter_Iterator()?;
875    let iter_mod = famous_defs.core_iter()?;
876
877    // Assert that this struct comes from `core::iter`.
878    if !(strukt.visibility(db) == hir::Visibility::Public
879        && strukt.module(db).path_to_root(db).contains(&iter_mod))
880    {
881        return None;
882    }
883
884    if ty.impls_trait(db, iter_trait, &[]) {
885        let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item {
886            hir::AssocItem::TypeAlias(alias) if alias.name(db) == sym::Item => Some(alias),
887            _ => None,
888        })?;
889        if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) {
890            return Some((iter_trait, assoc_type_item, ty));
891        }
892    }
893
894    None
895}
896
897fn ty_to_text_edit(
898    sema: &Semantics<'_, RootDatabase>,
899    config: &InlayHintsConfig<'_>,
900    node_for_hint: &SyntaxNode,
901    ty: &hir::Type<'_>,
902    offset_to_insert_ty: TextSize,
903    additional_edits: &dyn Fn(&mut TextEditBuilder),
904    prefix: impl Into<String>,
905) -> Option<LazyProperty<TextEdit>> {
906    // FIXME: Limit the length and bail out on excess somehow?
907    let rendered = sema
908        .scope(node_for_hint)
909        .and_then(|scope| ty.display_source_code(scope.db, scope.module().into(), false).ok())?;
910    Some(config.lazy_text_edit(|| {
911        let mut builder = TextEdit::builder();
912        builder.insert(offset_to_insert_ty, prefix.into());
913        builder.insert(offset_to_insert_ty, rendered);
914
915        additional_edits(&mut builder);
916
917        builder.finish()
918    }))
919}
920
921fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
922    matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
923}
924
925#[cfg(test)]
926mod tests {
927
928    use expect_test::Expect;
929    use hir::ClosureStyle;
930    use ide_db::ra_fixture::RaFixtureConfig;
931    use itertools::Itertools;
932    use test_utils::extract_annotations;
933
934    use crate::DiscriminantHints;
935    use crate::inlay_hints::{AdjustmentHints, AdjustmentHintsMode};
936    use crate::{LifetimeElisionHints, fixture, inlay_hints::InlayHintsConfig};
937
938    use super::{
939        ClosureReturnTypeHints, GenericParameterHints, ImplicitDropHints, InlayFieldsToResolve,
940        TypeHintsPlacement,
941    };
942
943    pub(super) const DISABLED_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
944        discriminant_hints: DiscriminantHints::Never,
945        render_colons: false,
946        type_hints: false,
947        type_hints_placement: TypeHintsPlacement::Inline,
948        parameter_hints: false,
949        parameter_hints_for_missing_arguments: false,
950        sized_bound: false,
951        generic_parameter_hints: GenericParameterHints {
952            type_hints: false,
953            lifetime_hints: false,
954            const_hints: false,
955        },
956        chaining_hints: false,
957        lifetime_elision_hints: LifetimeElisionHints::Never,
958        closure_return_type_hints: ClosureReturnTypeHints::Never,
959        closure_capture_hints: false,
960        adjustment_hints: AdjustmentHints::Never,
961        adjustment_hints_disable_reborrows: false,
962        adjustment_hints_mode: AdjustmentHintsMode::Prefix,
963        adjustment_hints_hide_outside_unsafe: false,
964        binding_mode_hints: false,
965        hide_inferred_type_hints: false,
966        hide_named_constructor_hints: false,
967        hide_closure_initialization_hints: false,
968        hide_closure_parameter_hints: false,
969        closure_style: ClosureStyle::ImplFn,
970        param_names_for_lifetime_elision_hints: false,
971        max_length: None,
972        closing_brace_hints_min_lines: None,
973        fields_to_resolve: InlayFieldsToResolve::empty(),
974        implicit_drop_hints: ImplicitDropHints::Never,
975        implied_dyn_trait_hints: false,
976        range_exclusive_hints: false,
977        ra_fixture: RaFixtureConfig::default(),
978    };
979    pub(super) const TEST_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
980        type_hints: true,
981        type_hints_placement: TypeHintsPlacement::Inline,
982        parameter_hints: true,
983        chaining_hints: true,
984        closure_return_type_hints: ClosureReturnTypeHints::WithBlock,
985        binding_mode_hints: true,
986        lifetime_elision_hints: LifetimeElisionHints::Always,
987        ..DISABLED_CONFIG
988    };
989
990    #[track_caller]
991    pub(super) fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
992        check_with_config(TEST_CONFIG, ra_fixture);
993    }
994
995    #[track_caller]
996    pub(super) fn check_with_config(
997        config: InlayHintsConfig<'_>,
998        #[rust_analyzer::rust_fixture] ra_fixture: &str,
999    ) {
1000        let (analysis, file_id) = fixture::file(ra_fixture);
1001        let mut expected = extract_annotations(&analysis.file_text(file_id).unwrap());
1002        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1003        let actual = inlay_hints
1004            .into_iter()
1005            // FIXME: We trim the start because some inlay produces leading whitespace which is not properly supported by our annotation extraction
1006            .map(|it| (it.range, it.label.to_string().trim_start().to_owned()))
1007            .sorted_by_key(|(range, _)| range.start())
1008            .collect::<Vec<_>>();
1009        expected.sort_by_key(|(range, _)| range.start());
1010
1011        assert_eq!(expected, actual, "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}");
1012    }
1013
1014    #[track_caller]
1015    pub(super) fn check_expect(
1016        config: InlayHintsConfig<'_>,
1017        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1018        expect: Expect,
1019    ) {
1020        let (analysis, file_id) = fixture::file(ra_fixture);
1021        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1022        let filtered =
1023            inlay_hints.into_iter().map(|hint| (hint.range, hint.label)).collect::<Vec<_>>();
1024        expect.assert_debug_eq(&filtered)
1025    }
1026
1027    /// Computes inlay hints for the fixture, applies all the provided text edits and then runs
1028    /// expect test.
1029    #[track_caller]
1030    pub(super) fn check_edit(
1031        config: InlayHintsConfig<'_>,
1032        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1033        expect: Expect,
1034    ) {
1035        let (analysis, file_id) = fixture::file(ra_fixture);
1036        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1037
1038        let edits = inlay_hints
1039            .into_iter()
1040            .filter_map(|hint| hint.text_edit?.computed())
1041            .reduce(|mut acc, next| {
1042                acc.union(next).expect("merging text edits failed");
1043                acc
1044            })
1045            .expect("no edit returned");
1046
1047        let mut actual = analysis.file_text(file_id).unwrap().to_string();
1048        edits.apply(&mut actual);
1049        expect.assert_eq(&actual);
1050    }
1051
1052    #[track_caller]
1053    pub(super) fn check_no_edit(
1054        config: InlayHintsConfig<'_>,
1055        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1056    ) {
1057        let (analysis, file_id) = fixture::file(ra_fixture);
1058        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1059
1060        let edits: Vec<_> =
1061            inlay_hints.into_iter().filter_map(|hint| hint.text_edit?.computed()).collect();
1062
1063        assert!(edits.is_empty(), "unexpected edits: {edits:?}");
1064    }
1065
1066    #[test]
1067    fn hints_disabled() {
1068        check_with_config(
1069            InlayHintsConfig { render_colons: true, ..DISABLED_CONFIG },
1070            r#"
1071fn foo(a: i32, b: i32) -> i32 { a + b }
1072fn main() {
1073    let _x = foo(4, 4);
1074}"#,
1075        );
1076    }
1077
1078    #[test]
1079    fn regression_18840() {
1080        check(
1081            r#"
1082//- proc_macros: issue_18840
1083#[proc_macros::issue_18840]
1084fn foo() {
1085    let
1086    loop {}
1087}
1088"#,
1089        );
1090    }
1091
1092    #[test]
1093    fn regression_18898() {
1094        check(
1095            r#"
1096//- proc_macros: issue_18898
1097#[proc_macros::issue_18898]
1098fn foo() {
1099    let
1100}
1101"#,
1102        );
1103    }
1104
1105    #[test]
1106    fn closure_dependency_cycle_no_panic() {
1107        check(
1108            r#"
1109//- minicore: fn
1110fn foo() {
1111    let closure;
1112     // ^^^^^^^ impl FnOnce()
1113    closure = || {
1114        closure();
1115    };
1116}
1117
1118fn bar() {
1119    let closure1;
1120     // ^^^^^^^^ impl FnOnce()
1121    let closure2;
1122     // ^^^^^^^^ impl FnOnce()
1123    closure1 = || {
1124        closure2();
1125    };
1126    closure2 = || {
1127        closure1();
1128    };
1129}
1130        "#,
1131        );
1132    }
1133
1134    #[test]
1135    fn regression_19610() {
1136        check(
1137            r#"
1138trait Trait {
1139    type Assoc;
1140}
1141struct Foo<A>(A);
1142impl<A: Trait<Assoc = impl Trait>> Foo<A> {
1143    fn foo<'a, 'b>(_: &'a [i32], _: &'b [i32]) {}
1144}
1145
1146fn bar() {
1147    Foo::foo(&[1], &[2]);
1148}
1149"#,
1150        );
1151    }
1152
1153    #[test]
1154    fn regression_20239() {
1155        check_with_config(
1156            InlayHintsConfig { parameter_hints: true, type_hints: true, ..DISABLED_CONFIG },
1157            r#"
1158//- minicore: fn
1159trait Iterator {
1160    type Item;
1161    fn map<B, F: FnMut(Self::Item) -> B>(self, f: F);
1162}
1163trait ToString {
1164    fn to_string(&self);
1165}
1166
1167fn check_tostr_eq<L, R>(left: L, right: R)
1168where
1169    L: Iterator,
1170    L::Item: ToString,
1171    R: Iterator,
1172    R::Item: ToString,
1173{
1174    left.map(|s| s.to_string());
1175           // ^ impl ToString
1176    right.map(|s| s.to_string());
1177            // ^ impl ToString
1178}
1179        "#,
1180        );
1181    }
1182}