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