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