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