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