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(hints, famous_defs, config, it.clone());
239                        closure_ret::hints(hints, famous_defs, config, display_target, it)
240                    },
241                    ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
242                    ast::Expr::Literal(it) => ra_fixture::hints(hints, famous_defs.0, file_id, config, it),
243                    _ => Some(()),
244                }
245            },
246            ast::Pat(it) => {
247                binding_mode::hints(hints, famous_defs, config, &it);
248                match it {
249                    ast::Pat::IdentPat(it) => {
250                        bind_pat::hints(hints, famous_defs, config, display_target, &it);
251                    }
252                    ast::Pat::RangePat(it) => {
253                        range_exclusive::hints(hints, famous_defs, config, it);
254                    }
255                    _ => {}
256                }
257                Some(())
258            },
259            ast::Item(it) => match it {
260                ast::Item::Fn(it) => {
261                    implicit_drop::hints(hints, famous_defs, config, display_target, &it);
262                    if let Some(extern_block) = &ctx.extern_block_parent {
263                        extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
264                    }
265                    lifetime::fn_hints(hints, ctx, famous_defs, config,  it)
266                },
267                ast::Item::Static(it) => {
268                    if let Some(extern_block) = &ctx.extern_block_parent {
269                        extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
270                    }
271                    implicit_static::hints(hints, famous_defs, config,  Either::Left(it))
272                },
273                ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
274                ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
275                ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
276                _ => None,
277            },
278            // FIXME: trait object type elisions
279            ast::Type(ty) => match ty {
280                ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config,  ptr),
281                ast::Type::PathType(path) => {
282                    lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
283                    implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
284                    Some(())
285                },
286                ast::Type::DynTraitType(dyn_) => {
287                    implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
288                    Some(())
289                },
290                ast::Type::InferType(placeholder) => {
291                    placeholders::type_hints(hints, famous_defs, config, display_target, placeholder);
292                    Some(())
293                },
294                _ => Some(()),
295            },
296            ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config,  it),
297            _ => Some(()),
298        }
299    };
300}
301
302#[derive(Clone, Debug)]
303pub struct InlayHintsConfig<'a> {
304    pub render_colons: bool,
305    pub type_hints: bool,
306    pub sized_bound: bool,
307    pub discriminant_hints: DiscriminantHints,
308    pub parameter_hints: bool,
309    pub parameter_hints_for_missing_arguments: bool,
310    pub generic_parameter_hints: GenericParameterHints,
311    pub chaining_hints: bool,
312    pub adjustment_hints: AdjustmentHints,
313    pub adjustment_hints_disable_reborrows: bool,
314    pub adjustment_hints_mode: AdjustmentHintsMode,
315    pub adjustment_hints_hide_outside_unsafe: bool,
316    pub closure_return_type_hints: ClosureReturnTypeHints,
317    pub closure_capture_hints: bool,
318    pub binding_mode_hints: bool,
319    pub implicit_drop_hints: bool,
320    pub implied_dyn_trait_hints: bool,
321    pub lifetime_elision_hints: LifetimeElisionHints,
322    pub param_names_for_lifetime_elision_hints: bool,
323    pub hide_inferred_type_hints: bool,
324    pub hide_named_constructor_hints: bool,
325    pub hide_closure_initialization_hints: bool,
326    pub hide_closure_parameter_hints: bool,
327    pub range_exclusive_hints: bool,
328    pub closure_style: ClosureStyle,
329    pub max_length: Option<usize>,
330    pub closing_brace_hints_min_lines: Option<usize>,
331    pub fields_to_resolve: InlayFieldsToResolve,
332    pub ra_fixture: RaFixtureConfig<'a>,
333}
334
335impl InlayHintsConfig<'_> {
336    fn lazy_text_edit(&self, finish: impl FnOnce() -> TextEdit) -> LazyProperty<TextEdit> {
337        if self.fields_to_resolve.resolve_text_edits {
338            LazyProperty::Lazy
339        } else {
340            let edit = finish();
341            never!(edit.is_empty(), "inlay hint produced an empty text edit");
342            LazyProperty::Computed(edit)
343        }
344    }
345
346    fn lazy_tooltip(&self, finish: impl FnOnce() -> InlayTooltip) -> LazyProperty<InlayTooltip> {
347        if self.fields_to_resolve.resolve_hint_tooltip
348            && self.fields_to_resolve.resolve_label_tooltip
349        {
350            LazyProperty::Lazy
351        } else {
352            let tooltip = finish();
353            never!(
354                match &tooltip {
355                    InlayTooltip::String(s) => s,
356                    InlayTooltip::Markdown(s) => s,
357                }
358                .is_empty(),
359                "inlay hint produced an empty tooltip"
360            );
361            LazyProperty::Computed(tooltip)
362        }
363    }
364
365    /// This always reports a resolvable location, so only use this when it is very likely for a
366    /// location link to actually resolve but where computing `finish` would be costly.
367    fn lazy_location_opt(
368        &self,
369        finish: impl FnOnce() -> Option<FileRange>,
370    ) -> Option<LazyProperty<FileRange>> {
371        if self.fields_to_resolve.resolve_label_location {
372            Some(LazyProperty::Lazy)
373        } else {
374            finish().map(LazyProperty::Computed)
375        }
376    }
377}
378
379#[derive(Copy, Clone, Debug, PartialEq, Eq)]
380pub struct InlayFieldsToResolve {
381    pub resolve_text_edits: bool,
382    pub resolve_hint_tooltip: bool,
383    pub resolve_label_tooltip: bool,
384    pub resolve_label_location: bool,
385    pub resolve_label_command: bool,
386}
387
388impl InlayFieldsToResolve {
389    pub fn from_client_capabilities(client_capability_fields: &FxHashSet<&str>) -> Self {
390        Self {
391            resolve_text_edits: client_capability_fields.contains("textEdits"),
392            resolve_hint_tooltip: client_capability_fields.contains("tooltip"),
393            resolve_label_tooltip: client_capability_fields.contains("label.tooltip"),
394            resolve_label_location: client_capability_fields.contains("label.location"),
395            resolve_label_command: client_capability_fields.contains("label.command"),
396        }
397    }
398
399    pub const fn empty() -> Self {
400        Self {
401            resolve_text_edits: false,
402            resolve_hint_tooltip: false,
403            resolve_label_tooltip: false,
404            resolve_label_location: false,
405            resolve_label_command: false,
406        }
407    }
408}
409
410#[derive(Clone, Debug, PartialEq, Eq)]
411pub enum ClosureReturnTypeHints {
412    Always,
413    WithBlock,
414    Never,
415}
416
417#[derive(Clone, Debug, PartialEq, Eq)]
418pub enum DiscriminantHints {
419    Always,
420    Never,
421    Fieldless,
422}
423
424#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct GenericParameterHints {
426    pub type_hints: bool,
427    pub lifetime_hints: bool,
428    pub const_hints: bool,
429}
430
431#[derive(Clone, Debug, PartialEq, Eq)]
432pub enum LifetimeElisionHints {
433    Always,
434    SkipTrivial,
435    Never,
436}
437
438#[derive(Clone, Debug, PartialEq, Eq)]
439pub enum AdjustmentHints {
440    Always,
441    BorrowsOnly,
442    Never,
443}
444
445#[derive(Copy, Clone, Debug, PartialEq, Eq)]
446pub enum AdjustmentHintsMode {
447    Prefix,
448    Postfix,
449    PreferPrefix,
450    PreferPostfix,
451}
452
453#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
454pub enum InlayKind {
455    Adjustment,
456    BindingMode,
457    Chaining,
458    ClosingBrace,
459    ClosureCapture,
460    Discriminant,
461    GenericParamList,
462    Lifetime,
463    Parameter,
464    GenericParameter,
465    Type,
466    Dyn,
467    Drop,
468    RangeExclusive,
469    ExternUnsafety,
470}
471
472#[derive(Debug, Hash)]
473pub enum InlayHintPosition {
474    Before,
475    After,
476}
477
478#[derive(Debug, UpmapFromRaFixture)]
479pub struct InlayHint {
480    /// The text range this inlay hint applies to.
481    pub range: TextRange,
482    pub position: InlayHintPosition,
483    pub pad_left: bool,
484    pub pad_right: bool,
485    /// The kind of this inlay hint.
486    pub kind: InlayKind,
487    /// The actual label to show in the inlay hint.
488    pub label: InlayHintLabel,
489    /// Text edit to apply when "accepting" this inlay hint.
490    pub text_edit: Option<LazyProperty<TextEdit>>,
491    /// Range to recompute inlay hints when trying to resolve for this hint. If this is none, the
492    /// hint does not support resolving.
493    pub resolve_parent: Option<TextRange>,
494}
495
496/// A type signaling that a value is either computed, or is available for computation.
497#[derive(Clone, Debug, Default, UpmapFromRaFixture)]
498pub enum LazyProperty<T> {
499    Computed(T),
500    #[default]
501    Lazy,
502}
503
504impl<T> LazyProperty<T> {
505    pub fn computed(self) -> Option<T> {
506        match self {
507            LazyProperty::Computed(it) => Some(it),
508            _ => None,
509        }
510    }
511
512    pub fn is_lazy(&self) -> bool {
513        matches!(self, Self::Lazy)
514    }
515}
516
517impl std::hash::Hash for InlayHint {
518    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
519        self.range.hash(state);
520        self.position.hash(state);
521        self.pad_left.hash(state);
522        self.pad_right.hash(state);
523        self.kind.hash(state);
524        self.label.hash(state);
525        mem::discriminant(&self.text_edit).hash(state);
526    }
527}
528
529impl InlayHint {
530    fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint {
531        InlayHint {
532            range,
533            kind,
534            label: InlayHintLabel::from(")"),
535            text_edit: None,
536            position: InlayHintPosition::After,
537            pad_left: false,
538            pad_right: false,
539            resolve_parent: None,
540        }
541    }
542}
543
544#[derive(Debug, Hash)]
545pub enum InlayTooltip {
546    String(String),
547    Markdown(String),
548}
549
550#[derive(Default, Hash, UpmapFromRaFixture)]
551pub struct InlayHintLabel {
552    pub parts: SmallVec<[InlayHintLabelPart; 1]>,
553}
554
555impl InlayHintLabel {
556    pub fn simple(
557        s: impl Into<String>,
558        tooltip: Option<LazyProperty<InlayTooltip>>,
559        linked_location: Option<LazyProperty<FileRange>>,
560    ) -> InlayHintLabel {
561        InlayHintLabel {
562            parts: smallvec![InlayHintLabelPart { text: s.into(), linked_location, tooltip }],
563        }
564    }
565
566    pub fn prepend_str(&mut self, s: &str) {
567        match &mut *self.parts {
568            [InlayHintLabelPart { text, linked_location: None, tooltip: None }, ..] => {
569                text.insert_str(0, s)
570            }
571            _ => self.parts.insert(
572                0,
573                InlayHintLabelPart { text: s.into(), linked_location: None, tooltip: None },
574            ),
575        }
576    }
577
578    pub fn append_str(&mut self, s: &str) {
579        match &mut *self.parts {
580            [.., InlayHintLabelPart { text, linked_location: None, tooltip: None }] => {
581                text.push_str(s)
582            }
583            _ => self.parts.push(InlayHintLabelPart {
584                text: s.into(),
585                linked_location: None,
586                tooltip: None,
587            }),
588        }
589    }
590
591    pub fn append_part(&mut self, part: InlayHintLabelPart) {
592        if part.linked_location.is_none()
593            && part.tooltip.is_none()
594            && let Some(InlayHintLabelPart { text, linked_location: None, tooltip: None }) =
595                self.parts.last_mut()
596        {
597            text.push_str(&part.text);
598            return;
599        }
600        self.parts.push(part);
601    }
602}
603
604impl From<String> for InlayHintLabel {
605    fn from(s: String) -> Self {
606        Self {
607            parts: smallvec![InlayHintLabelPart { text: s, linked_location: None, tooltip: None }],
608        }
609    }
610}
611
612impl From<&str> for InlayHintLabel {
613    fn from(s: &str) -> Self {
614        Self {
615            parts: smallvec![InlayHintLabelPart {
616                text: s.into(),
617                linked_location: None,
618                tooltip: None
619            }],
620        }
621    }
622}
623
624impl fmt::Display for InlayHintLabel {
625    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626        write!(f, "{}", self.parts.iter().map(|part| &part.text).format(""))
627    }
628}
629
630impl fmt::Debug for InlayHintLabel {
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        f.debug_list().entries(&self.parts).finish()
633    }
634}
635
636#[derive(UpmapFromRaFixture)]
637pub struct InlayHintLabelPart {
638    pub text: String,
639    /// Source location represented by this label part. The client will use this to fetch the part's
640    /// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location
641    /// refers to (not necessarily the location itself).
642    /// When setting this, no tooltip must be set on the containing hint, or VS Code will display
643    /// them both.
644    pub linked_location: Option<LazyProperty<FileRange>>,
645    /// The tooltip to show when hovering over the inlay hint, this may invoke other actions like
646    /// hover requests to show.
647    pub tooltip: Option<LazyProperty<InlayTooltip>>,
648}
649
650impl std::hash::Hash for InlayHintLabelPart {
651    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
652        self.text.hash(state);
653        self.linked_location.is_some().hash(state);
654        self.tooltip.is_some().hash(state);
655    }
656}
657
658impl fmt::Debug for InlayHintLabelPart {
659    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660        match self {
661            Self { text, linked_location: None, tooltip: None | Some(LazyProperty::Lazy) } => {
662                text.fmt(f)
663            }
664            Self { text, linked_location, tooltip } => f
665                .debug_struct("InlayHintLabelPart")
666                .field("text", text)
667                .field("linked_location", linked_location)
668                .field(
669                    "tooltip",
670                    &tooltip.as_ref().map_or("", |it| match it {
671                        LazyProperty::Computed(
672                            InlayTooltip::String(it) | InlayTooltip::Markdown(it),
673                        ) => it,
674                        LazyProperty::Lazy => "",
675                    }),
676                )
677                .finish(),
678        }
679    }
680}
681
682#[derive(Debug)]
683struct InlayHintLabelBuilder<'a> {
684    sema: &'a Semantics<'a, RootDatabase>,
685    result: InlayHintLabel,
686    last_part: String,
687    resolve: bool,
688    location: Option<LazyProperty<FileRange>>,
689}
690
691impl fmt::Write for InlayHintLabelBuilder<'_> {
692    fn write_str(&mut self, s: &str) -> fmt::Result {
693        self.last_part.write_str(s)
694    }
695}
696
697impl HirWrite for InlayHintLabelBuilder<'_> {
698    fn start_location_link(&mut self, def: ModuleDefId) {
699        never!(self.location.is_some(), "location link is already started");
700        self.make_new_part();
701
702        self.location = Some(if self.resolve {
703            LazyProperty::Lazy
704        } else {
705            LazyProperty::Computed({
706                let Some(location) = ModuleDef::from(def).try_to_nav(self.sema) else { return };
707                let location = location.call_site();
708                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
709            })
710        });
711    }
712
713    fn start_location_link_generic(&mut self, def: GenericParamId) {
714        never!(self.location.is_some(), "location link is already started");
715        self.make_new_part();
716
717        self.location = Some(if self.resolve {
718            LazyProperty::Lazy
719        } else {
720            LazyProperty::Computed({
721                let Some(location) = GenericParam::from(def).try_to_nav(self.sema) else { return };
722                let location = location.call_site();
723                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
724            })
725        });
726    }
727
728    fn end_location_link(&mut self) {
729        self.make_new_part();
730    }
731}
732
733impl InlayHintLabelBuilder<'_> {
734    fn make_new_part(&mut self) {
735        let text = take(&mut self.last_part);
736        if !text.is_empty() {
737            self.result.parts.push(InlayHintLabelPart {
738                text,
739                linked_location: self.location.take(),
740                tooltip: None,
741            });
742        }
743    }
744
745    fn finish(mut self) -> InlayHintLabel {
746        self.make_new_part();
747        self.result
748    }
749}
750
751fn label_of_ty(
752    famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
753    config: &InlayHintsConfig<'_>,
754    ty: &hir::Type<'_>,
755    display_target: DisplayTarget,
756) -> Option<InlayHintLabel> {
757    fn rec(
758        sema: &Semantics<'_, RootDatabase>,
759        famous_defs: &FamousDefs<'_, '_>,
760        mut max_length: Option<usize>,
761        ty: &hir::Type<'_>,
762        label_builder: &mut InlayHintLabelBuilder<'_>,
763        config: &InlayHintsConfig<'_>,
764        display_target: DisplayTarget,
765    ) -> Result<(), HirDisplayError> {
766        let iter_item_type = hint_iterator(sema, famous_defs, ty);
767        match iter_item_type {
768            Some((iter_trait, item, ty)) => {
769                const LABEL_START: &str = "impl ";
770                const LABEL_ITERATOR: &str = "Iterator";
771                const LABEL_MIDDLE: &str = "<";
772                const LABEL_ITEM: &str = "Item";
773                const LABEL_MIDDLE2: &str = " = ";
774                const LABEL_END: &str = ">";
775
776                max_length = max_length.map(|len| {
777                    len.saturating_sub(
778                        LABEL_START.len()
779                            + LABEL_ITERATOR.len()
780                            + LABEL_MIDDLE.len()
781                            + LABEL_MIDDLE2.len()
782                            + LABEL_END.len(),
783                    )
784                });
785
786                let module_def_location = |label_builder: &mut InlayHintLabelBuilder<'_>,
787                                           def: ModuleDef,
788                                           name| {
789                    let def = def.try_into();
790                    if let Ok(def) = def {
791                        label_builder.start_location_link(def);
792                    }
793                    #[expect(
794                        clippy::question_mark,
795                        reason = "false positive; replacing with `?` leads to 'type annotations needed' error"
796                    )]
797                    if let Err(err) = label_builder.write_str(name) {
798                        return Err(err);
799                    }
800                    if def.is_ok() {
801                        label_builder.end_location_link();
802                    }
803                    Ok(())
804                };
805
806                label_builder.write_str(LABEL_START)?;
807                module_def_location(label_builder, ModuleDef::from(iter_trait), LABEL_ITERATOR)?;
808                label_builder.write_str(LABEL_MIDDLE)?;
809                module_def_location(label_builder, ModuleDef::from(item), LABEL_ITEM)?;
810                label_builder.write_str(LABEL_MIDDLE2)?;
811                rec(sema, famous_defs, max_length, &ty, label_builder, config, display_target)?;
812                label_builder.write_str(LABEL_END)?;
813                Ok(())
814            }
815            None => ty
816                .display_truncated(sema.db, max_length, display_target)
817                .with_closure_style(config.closure_style)
818                .write_to(label_builder),
819        }
820    }
821
822    let mut label_builder = InlayHintLabelBuilder {
823        sema,
824        last_part: String::new(),
825        location: None,
826        result: InlayHintLabel::default(),
827        resolve: config.fields_to_resolve.resolve_label_location,
828    };
829    let _ =
830        rec(sema, famous_defs, config.max_length, ty, &mut label_builder, config, display_target);
831    let r = label_builder.finish();
832    Some(r)
833}
834
835/// Checks if the type is an Iterator from std::iter and returns the iterator trait and the item type of the concrete iterator.
836fn hint_iterator<'db>(
837    sema: &Semantics<'db, RootDatabase>,
838    famous_defs: &FamousDefs<'_, 'db>,
839    ty: &hir::Type<'db>,
840) -> Option<(hir::Trait, hir::TypeAlias, hir::Type<'db>)> {
841    let db = sema.db;
842    let strukt = ty.strip_references().as_adt()?;
843    let krate = strukt.module(db).krate(db);
844    if krate != famous_defs.core()? {
845        return None;
846    }
847    let iter_trait = famous_defs.core_iter_Iterator()?;
848    let iter_mod = famous_defs.core_iter()?;
849
850    // Assert that this struct comes from `core::iter`.
851    if !(strukt.visibility(db) == hir::Visibility::Public
852        && strukt.module(db).path_to_root(db).contains(&iter_mod))
853    {
854        return None;
855    }
856
857    if ty.impls_trait(db, iter_trait, &[]) {
858        let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item {
859            hir::AssocItem::TypeAlias(alias) if alias.name(db) == sym::Item => Some(alias),
860            _ => None,
861        })?;
862        if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) {
863            return Some((iter_trait, assoc_type_item, ty));
864        }
865    }
866
867    None
868}
869
870fn ty_to_text_edit(
871    sema: &Semantics<'_, RootDatabase>,
872    config: &InlayHintsConfig<'_>,
873    node_for_hint: &SyntaxNode,
874    ty: &hir::Type<'_>,
875    offset_to_insert_ty: TextSize,
876    additional_edits: &dyn Fn(&mut TextEditBuilder),
877    prefix: impl Into<String>,
878) -> Option<LazyProperty<TextEdit>> {
879    // FIXME: Limit the length and bail out on excess somehow?
880    let rendered = sema
881        .scope(node_for_hint)
882        .and_then(|scope| ty.display_source_code(scope.db, scope.module().into(), false).ok())?;
883    Some(config.lazy_text_edit(|| {
884        let mut builder = TextEdit::builder();
885        builder.insert(offset_to_insert_ty, prefix.into());
886        builder.insert(offset_to_insert_ty, rendered);
887
888        additional_edits(&mut builder);
889
890        builder.finish()
891    }))
892}
893
894fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
895    matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
896}
897
898#[cfg(test)]
899mod tests {
900
901    use expect_test::Expect;
902    use hir::ClosureStyle;
903    use ide_db::ra_fixture::RaFixtureConfig;
904    use itertools::Itertools;
905    use test_utils::extract_annotations;
906
907    use crate::DiscriminantHints;
908    use crate::inlay_hints::{AdjustmentHints, AdjustmentHintsMode};
909    use crate::{LifetimeElisionHints, fixture, inlay_hints::InlayHintsConfig};
910
911    use super::{ClosureReturnTypeHints, GenericParameterHints, InlayFieldsToResolve};
912
913    pub(super) const DISABLED_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
914        discriminant_hints: DiscriminantHints::Never,
915        render_colons: false,
916        type_hints: false,
917        parameter_hints: false,
918        parameter_hints_for_missing_arguments: false,
919        sized_bound: false,
920        generic_parameter_hints: GenericParameterHints {
921            type_hints: false,
922            lifetime_hints: false,
923            const_hints: false,
924        },
925        chaining_hints: false,
926        lifetime_elision_hints: LifetimeElisionHints::Never,
927        closure_return_type_hints: ClosureReturnTypeHints::Never,
928        closure_capture_hints: false,
929        adjustment_hints: AdjustmentHints::Never,
930        adjustment_hints_disable_reborrows: false,
931        adjustment_hints_mode: AdjustmentHintsMode::Prefix,
932        adjustment_hints_hide_outside_unsafe: false,
933        binding_mode_hints: false,
934        hide_inferred_type_hints: false,
935        hide_named_constructor_hints: false,
936        hide_closure_initialization_hints: false,
937        hide_closure_parameter_hints: false,
938        closure_style: ClosureStyle::ImplFn,
939        param_names_for_lifetime_elision_hints: false,
940        max_length: None,
941        closing_brace_hints_min_lines: None,
942        fields_to_resolve: InlayFieldsToResolve::empty(),
943        implicit_drop_hints: false,
944        implied_dyn_trait_hints: false,
945        range_exclusive_hints: false,
946        ra_fixture: RaFixtureConfig::default(),
947    };
948    pub(super) const TEST_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
949        type_hints: true,
950        parameter_hints: true,
951        chaining_hints: true,
952        closure_return_type_hints: ClosureReturnTypeHints::WithBlock,
953        binding_mode_hints: true,
954        lifetime_elision_hints: LifetimeElisionHints::Always,
955        ..DISABLED_CONFIG
956    };
957
958    #[track_caller]
959    pub(super) fn check(#[rust_analyzer::rust_fixture] ra_fixture: &str) {
960        check_with_config(TEST_CONFIG, ra_fixture);
961    }
962
963    #[track_caller]
964    pub(super) fn check_with_config(
965        config: InlayHintsConfig<'_>,
966        #[rust_analyzer::rust_fixture] ra_fixture: &str,
967    ) {
968        let (analysis, file_id) = fixture::file(ra_fixture);
969        let mut expected = extract_annotations(&analysis.file_text(file_id).unwrap());
970        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
971        let actual = inlay_hints
972            .into_iter()
973            // FIXME: We trim the start because some inlay produces leading whitespace which is not properly supported by our annotation extraction
974            .map(|it| (it.range, it.label.to_string().trim_start().to_owned()))
975            .sorted_by_key(|(range, _)| range.start())
976            .collect::<Vec<_>>();
977        expected.sort_by_key(|(range, _)| range.start());
978
979        assert_eq!(expected, actual, "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}");
980    }
981
982    #[track_caller]
983    pub(super) fn check_expect(
984        config: InlayHintsConfig<'_>,
985        #[rust_analyzer::rust_fixture] ra_fixture: &str,
986        expect: Expect,
987    ) {
988        let (analysis, file_id) = fixture::file(ra_fixture);
989        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
990        let filtered =
991            inlay_hints.into_iter().map(|hint| (hint.range, hint.label)).collect::<Vec<_>>();
992        expect.assert_debug_eq(&filtered)
993    }
994
995    /// Computes inlay hints for the fixture, applies all the provided text edits and then runs
996    /// expect test.
997    #[track_caller]
998    pub(super) fn check_edit(
999        config: InlayHintsConfig<'_>,
1000        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1001        expect: Expect,
1002    ) {
1003        let (analysis, file_id) = fixture::file(ra_fixture);
1004        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1005
1006        let edits = inlay_hints
1007            .into_iter()
1008            .filter_map(|hint| hint.text_edit?.computed())
1009            .reduce(|mut acc, next| {
1010                acc.union(next).expect("merging text edits failed");
1011                acc
1012            })
1013            .expect("no edit returned");
1014
1015        let mut actual = analysis.file_text(file_id).unwrap().to_string();
1016        edits.apply(&mut actual);
1017        expect.assert_eq(&actual);
1018    }
1019
1020    #[track_caller]
1021    pub(super) fn check_no_edit(
1022        config: InlayHintsConfig<'_>,
1023        #[rust_analyzer::rust_fixture] ra_fixture: &str,
1024    ) {
1025        let (analysis, file_id) = fixture::file(ra_fixture);
1026        let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
1027
1028        let edits: Vec<_> =
1029            inlay_hints.into_iter().filter_map(|hint| hint.text_edit?.computed()).collect();
1030
1031        assert!(edits.is_empty(), "unexpected edits: {edits:?}");
1032    }
1033
1034    #[test]
1035    fn hints_disabled() {
1036        check_with_config(
1037            InlayHintsConfig { render_colons: true, ..DISABLED_CONFIG },
1038            r#"
1039fn foo(a: i32, b: i32) -> i32 { a + b }
1040fn main() {
1041    let _x = foo(4, 4);
1042}"#,
1043        );
1044    }
1045
1046    #[test]
1047    fn regression_18840() {
1048        check(
1049            r#"
1050//- proc_macros: issue_18840
1051#[proc_macros::issue_18840]
1052fn foo() {
1053    let
1054    loop {}
1055}
1056"#,
1057        );
1058    }
1059
1060    #[test]
1061    fn regression_18898() {
1062        check(
1063            r#"
1064//- proc_macros: issue_18898
1065#[proc_macros::issue_18898]
1066fn foo() {
1067    let
1068}
1069"#,
1070        );
1071    }
1072
1073    #[test]
1074    fn closure_dependency_cycle_no_panic() {
1075        check(
1076            r#"
1077fn foo() {
1078    let closure;
1079     // ^^^^^^^ impl Fn()
1080    closure = || {
1081        closure();
1082    };
1083}
1084
1085fn bar() {
1086    let closure1;
1087     // ^^^^^^^^ impl Fn()
1088    let closure2;
1089     // ^^^^^^^^ impl Fn()
1090    closure1 = || {
1091        closure2();
1092    };
1093    closure2 = || {
1094        closure1();
1095    };
1096}
1097        "#,
1098        );
1099    }
1100
1101    #[test]
1102    fn regression_19610() {
1103        check(
1104            r#"
1105trait Trait {
1106    type Assoc;
1107}
1108struct Foo<A>(A);
1109impl<A: Trait<Assoc = impl Trait>> Foo<A> {
1110    fn foo<'a, 'b>(_: &'a [i32], _: &'b [i32]) {}
1111}
1112
1113fn bar() {
1114    Foo::foo(&[1], &[2]);
1115}
1116"#,
1117        );
1118    }
1119
1120    #[test]
1121    fn regression_20239() {
1122        check_with_config(
1123            InlayHintsConfig { parameter_hints: true, type_hints: true, ..DISABLED_CONFIG },
1124            r#"
1125//- minicore: fn
1126trait Iterator {
1127    type Item;
1128    fn map<B, F: FnMut(Self::Item) -> B>(self, f: F);
1129}
1130trait ToString {
1131    fn to_string(&self);
1132}
1133
1134fn check_tostr_eq<L, R>(left: L, right: R)
1135where
1136    L: Iterator,
1137    L::Item: ToString,
1138    R: Iterator,
1139    R::Item: ToString,
1140{
1141    left.map(|s| s.to_string());
1142           // ^ impl ToString
1143    right.map(|s| s.to_string());
1144            // ^ impl ToString
1145}
1146        "#,
1147        );
1148    }
1149}