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.attach_first_edition(file_id);
93    let file = sema.parse(file_id);
94    let file = file.syntax();
95
96    let mut acc = Vec::new();
97
98    let Some(scope) = sema.scope(file) else {
99        return acc;
100    };
101    let famous_defs = FamousDefs(&sema, scope.krate());
102    let display_target = famous_defs.1.to_display_target(sema.db);
103
104    let ctx = &mut InlayHintCtx::default();
105    let mut hints = |event| {
106        if let Some(node) = handle_event(ctx, event) {
107            hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
108        }
109    };
110    let mut preorder = file.preorder();
111    hir::attach_db(sema.db, || {
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    });
121    if let Some(range_limit) = range_limit {
122        acc.retain(|hint| range_limit.contains_range(hint.range));
123    }
124    acc
125}
126
127#[derive(Default)]
128struct InlayHintCtx {
129    lifetime_stacks: Vec<Vec<SmolStr>>,
130    extern_block_parent: Option<ast::ExternBlock>,
131}
132
133pub(crate) fn inlay_hints_resolve(
134    db: &RootDatabase,
135    file_id: FileId,
136    resolve_range: TextRange,
137    hash: u64,
138    config: &InlayHintsConfig<'_>,
139    hasher: impl Fn(&InlayHint) -> u64,
140) -> Option<InlayHint> {
141    let _p = tracing::info_span!("inlay_hints_resolve").entered();
142    let sema = Semantics::new(db);
143    let file_id = sema.attach_first_edition(file_id);
144    let file = sema.parse(file_id);
145    let file = file.syntax();
146
147    let scope = sema.scope(file)?;
148    let famous_defs = FamousDefs(&sema, scope.krate());
149    let mut acc = Vec::new();
150
151    let display_target = famous_defs.1.to_display_target(sema.db);
152
153    let ctx = &mut InlayHintCtx::default();
154    let mut hints = |event| {
155        if let Some(node) = handle_event(ctx, event) {
156            hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
157        }
158    };
159
160    let mut preorder = file.preorder();
161    while let Some(event) = preorder.next() {
162        // FIXME: This can miss some hints that require the parent of the range to calculate
163        if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
164        {
165            preorder.skip_subtree();
166            continue;
167        }
168        hints(event);
169    }
170    acc.into_iter().find(|hint| hasher(hint) == hash)
171}
172
173fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
174    match node {
175        WalkEvent::Enter(node) => {
176            if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
177                let params = node
178                    .generic_param_list()
179                    .map(|it| {
180                        it.lifetime_params()
181                            .filter_map(|it| {
182                                it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
183                            })
184                            .collect()
185                    })
186                    .unwrap_or_default();
187                ctx.lifetime_stacks.push(params);
188            }
189            if let Some(node) = ast::ExternBlock::cast(node.clone()) {
190                ctx.extern_block_parent = Some(node);
191            }
192            Some(node)
193        }
194        WalkEvent::Leave(n) => {
195            if ast::AnyHasGenericParams::can_cast(n.kind()) {
196                ctx.lifetime_stacks.pop();
197            }
198            if ast::ExternBlock::can_cast(n.kind()) {
199                ctx.extern_block_parent = None;
200            }
201            None
202        }
203    }
204}
205
206// FIXME: At some point when our hir infra is fleshed out enough we should flip this and traverse the
207// HIR instead of the syntax tree.
208fn hints(
209    hints: &mut Vec<InlayHint>,
210    ctx: &mut InlayHintCtx,
211    famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
212    config: &InlayHintsConfig<'_>,
213    file_id: EditionedFileId,
214    display_target: DisplayTarget,
215    node: SyntaxNode,
216) {
217    closing_brace::hints(
218        hints,
219        sema,
220        config,
221        display_target,
222        InRealFile { file_id, value: node.clone() },
223    );
224    if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
225        generic_param::hints(hints, famous_defs, config, any_has_generic_args);
226    }
227
228    match_ast! {
229        match node {
230            ast::Expr(expr) => {
231                chaining::hints(hints, famous_defs, config, display_target, &expr);
232                adjustment::hints(hints, famous_defs, config, display_target, &expr);
233                match expr {
234                    ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
235                    ast::Expr::MethodCallExpr(it) => {
236                        param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
237                    }
238                    ast::Expr::ClosureExpr(it) => {
239                        closure_captures::hints(hints, famous_defs, config, it.clone());
240                        closure_ret::hints(hints, famous_defs, config, display_target, it)
241                    },
242                    ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
243                    ast::Expr::Literal(it) => ra_fixture::hints(hints, famous_defs.0, file_id, config, it),
244                    _ => Some(()),
245                }
246            },
247            ast::Pat(it) => {
248                binding_mode::hints(hints, famous_defs, config, &it);
249                match it {
250                    ast::Pat::IdentPat(it) => {
251                        bind_pat::hints(hints, famous_defs, config, display_target, &it);
252                    }
253                    ast::Pat::RangePat(it) => {
254                        range_exclusive::hints(hints, famous_defs, config, it);
255                    }
256                    _ => {}
257                }
258                Some(())
259            },
260            ast::Item(it) => match it {
261                ast::Item::Fn(it) => {
262                    implicit_drop::hints(hints, famous_defs, config, display_target, &it);
263                    if let Some(extern_block) = &ctx.extern_block_parent {
264                        extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
265                    }
266                    lifetime::fn_hints(hints, ctx, famous_defs, config,  it)
267                },
268                ast::Item::Static(it) => {
269                    if let Some(extern_block) = &ctx.extern_block_parent {
270                        extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
271                    }
272                    implicit_static::hints(hints, famous_defs, config,  Either::Left(it))
273                },
274                ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
275                ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
276                ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
277                _ => None,
278            },
279            // FIXME: trait object type elisions
280            ast::Type(ty) => match ty {
281                ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config,  ptr),
282                ast::Type::PathType(path) => {
283                    lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
284                    implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
285                    Some(())
286                },
287                ast::Type::DynTraitType(dyn_) => {
288                    implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
289                    Some(())
290                },
291                ast::Type::InferType(placeholder) => {
292                    placeholders::type_hints(hints, famous_defs, config, display_target, placeholder);
293                    Some(())
294                },
295                _ => Some(()),
296            },
297            ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config,  it),
298            _ => Some(()),
299        }
300    };
301}
302
303#[derive(Clone, Debug)]
304pub struct InlayHintsConfig<'a> {
305    pub render_colons: bool,
306    pub type_hints: bool,
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 minicore: MiniCore<'a>,
334}
335
336impl InlayHintsConfig<'_> {
337    fn lazy_text_edit(&self, finish: impl FnOnce() -> TextEdit) -> LazyProperty<TextEdit> {
338        if self.fields_to_resolve.resolve_text_edits {
339            LazyProperty::Lazy
340        } else {
341            let edit = finish();
342            never!(edit.is_empty(), "inlay hint produced an empty text edit");
343            LazyProperty::Computed(edit)
344        }
345    }
346
347    fn lazy_tooltip(&self, finish: impl FnOnce() -> InlayTooltip) -> LazyProperty<InlayTooltip> {
348        if self.fields_to_resolve.resolve_hint_tooltip
349            && self.fields_to_resolve.resolve_label_tooltip
350        {
351            LazyProperty::Lazy
352        } else {
353            let tooltip = finish();
354            never!(
355                match &tooltip {
356                    InlayTooltip::String(s) => s,
357                    InlayTooltip::Markdown(s) => s,
358                }
359                .is_empty(),
360                "inlay hint produced an empty tooltip"
361            );
362            LazyProperty::Computed(tooltip)
363        }
364    }
365
366    /// This always reports a resolvable location, so only use this when it is very likely for a
367    /// location link to actually resolve but where computing `finish` would be costly.
368    fn lazy_location_opt(
369        &self,
370        finish: impl FnOnce() -> Option<FileRange>,
371    ) -> Option<LazyProperty<FileRange>> {
372        if self.fields_to_resolve.resolve_label_location {
373            Some(LazyProperty::Lazy)
374        } else {
375            finish().map(LazyProperty::Computed)
376        }
377    }
378}
379
380#[derive(Copy, Clone, Debug, PartialEq, Eq)]
381pub struct InlayFieldsToResolve {
382    pub resolve_text_edits: bool,
383    pub resolve_hint_tooltip: bool,
384    pub resolve_label_tooltip: bool,
385    pub resolve_label_location: bool,
386    pub resolve_label_command: bool,
387}
388
389impl InlayFieldsToResolve {
390    pub fn from_client_capabilities(client_capability_fields: &FxHashSet<&str>) -> Self {
391        Self {
392            resolve_text_edits: client_capability_fields.contains("textEdits"),
393            resolve_hint_tooltip: client_capability_fields.contains("tooltip"),
394            resolve_label_tooltip: client_capability_fields.contains("label.tooltip"),
395            resolve_label_location: client_capability_fields.contains("label.location"),
396            resolve_label_command: client_capability_fields.contains("label.command"),
397        }
398    }
399
400    pub const fn empty() -> Self {
401        Self {
402            resolve_text_edits: false,
403            resolve_hint_tooltip: false,
404            resolve_label_tooltip: false,
405            resolve_label_location: false,
406            resolve_label_command: false,
407        }
408    }
409}
410
411#[derive(Clone, Debug, PartialEq, Eq)]
412pub enum ClosureReturnTypeHints {
413    Always,
414    WithBlock,
415    Never,
416}
417
418#[derive(Clone, Debug, PartialEq, Eq)]
419pub enum DiscriminantHints {
420    Always,
421    Never,
422    Fieldless,
423}
424
425#[derive(Clone, Debug, PartialEq, Eq)]
426pub struct GenericParameterHints {
427    pub type_hints: bool,
428    pub lifetime_hints: bool,
429    pub const_hints: bool,
430}
431
432#[derive(Clone, Debug, PartialEq, Eq)]
433pub enum LifetimeElisionHints {
434    Always,
435    SkipTrivial,
436    Never,
437}
438
439#[derive(Clone, Debug, PartialEq, Eq)]
440pub enum AdjustmentHints {
441    Always,
442    BorrowsOnly,
443    Never,
444}
445
446#[derive(Copy, Clone, Debug, PartialEq, Eq)]
447pub enum AdjustmentHintsMode {
448    Prefix,
449    Postfix,
450    PreferPrefix,
451    PreferPostfix,
452}
453
454#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
455pub enum InlayKind {
456    Adjustment,
457    BindingMode,
458    Chaining,
459    ClosingBrace,
460    ClosureCapture,
461    Discriminant,
462    GenericParamList,
463    Lifetime,
464    Parameter,
465    GenericParameter,
466    Type,
467    Dyn,
468    Drop,
469    RangeExclusive,
470    ExternUnsafety,
471}
472
473#[derive(Debug, Hash)]
474pub enum InlayHintPosition {
475    Before,
476    After,
477}
478
479#[derive(Debug, UpmapFromRaFixture)]
480pub struct InlayHint {
481    /// The text range this inlay hint applies to.
482    pub range: TextRange,
483    pub position: InlayHintPosition,
484    pub pad_left: bool,
485    pub pad_right: bool,
486    /// The kind of this inlay hint.
487    pub kind: InlayKind,
488    /// The actual label to show in the inlay hint.
489    pub label: InlayHintLabel,
490    /// Text edit to apply when "accepting" this inlay hint.
491    pub text_edit: Option<LazyProperty<TextEdit>>,
492    /// Range to recompute inlay hints when trying to resolve for this hint. If this is none, the
493    /// hint does not support resolving.
494    pub resolve_parent: Option<TextRange>,
495}
496
497/// A type signaling that a value is either computed, or is available for computation.
498#[derive(Clone, Debug, Default, UpmapFromRaFixture)]
499pub enum LazyProperty<T> {
500    Computed(T),
501    #[default]
502    Lazy,
503}
504
505impl<T> LazyProperty<T> {
506    pub fn computed(self) -> Option<T> {
507        match self {
508            LazyProperty::Computed(it) => Some(it),
509            _ => None,
510        }
511    }
512
513    pub fn is_lazy(&self) -> bool {
514        matches!(self, Self::Lazy)
515    }
516}
517
518impl std::hash::Hash for InlayHint {
519    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
520        self.range.hash(state);
521        self.position.hash(state);
522        self.pad_left.hash(state);
523        self.pad_right.hash(state);
524        self.kind.hash(state);
525        self.label.hash(state);
526        mem::discriminant(&self.text_edit).hash(state);
527    }
528}
529
530impl InlayHint {
531    fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint {
532        InlayHint {
533            range,
534            kind,
535            label: InlayHintLabel::from(")"),
536            text_edit: None,
537            position: InlayHintPosition::After,
538            pad_left: false,
539            pad_right: false,
540            resolve_parent: None,
541        }
542    }
543}
544
545#[derive(Debug, Hash)]
546pub enum InlayTooltip {
547    String(String),
548    Markdown(String),
549}
550
551#[derive(Default, Hash, UpmapFromRaFixture)]
552pub struct InlayHintLabel {
553    pub parts: SmallVec<[InlayHintLabelPart; 1]>,
554}
555
556impl InlayHintLabel {
557    pub fn simple(
558        s: impl Into<String>,
559        tooltip: Option<LazyProperty<InlayTooltip>>,
560        linked_location: Option<LazyProperty<FileRange>>,
561    ) -> InlayHintLabel {
562        InlayHintLabel {
563            parts: smallvec![InlayHintLabelPart { text: s.into(), linked_location, tooltip }],
564        }
565    }
566
567    pub fn prepend_str(&mut self, s: &str) {
568        match &mut *self.parts {
569            [InlayHintLabelPart { text, linked_location: None, tooltip: None }, ..] => {
570                text.insert_str(0, s)
571            }
572            _ => self.parts.insert(
573                0,
574                InlayHintLabelPart { text: s.into(), linked_location: None, tooltip: None },
575            ),
576        }
577    }
578
579    pub fn append_str(&mut self, s: &str) {
580        match &mut *self.parts {
581            [.., InlayHintLabelPart { text, linked_location: None, tooltip: None }] => {
582                text.push_str(s)
583            }
584            _ => self.parts.push(InlayHintLabelPart {
585                text: s.into(),
586                linked_location: None,
587                tooltip: None,
588            }),
589        }
590    }
591
592    pub fn append_part(&mut self, part: InlayHintLabelPart) {
593        if part.linked_location.is_none()
594            && part.tooltip.is_none()
595            && let Some(InlayHintLabelPart { text, linked_location: None, tooltip: None }) =
596                self.parts.last_mut()
597        {
598            text.push_str(&part.text);
599            return;
600        }
601        self.parts.push(part);
602    }
603}
604
605impl From<String> for InlayHintLabel {
606    fn from(s: String) -> Self {
607        Self {
608            parts: smallvec![InlayHintLabelPart { text: s, linked_location: None, tooltip: None }],
609        }
610    }
611}
612
613impl From<&str> for InlayHintLabel {
614    fn from(s: &str) -> Self {
615        Self {
616            parts: smallvec![InlayHintLabelPart {
617                text: s.into(),
618                linked_location: None,
619                tooltip: None
620            }],
621        }
622    }
623}
624
625impl fmt::Display for InlayHintLabel {
626    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627        write!(f, "{}", self.parts.iter().map(|part| &part.text).format(""))
628    }
629}
630
631impl fmt::Debug for InlayHintLabel {
632    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633        f.debug_list().entries(&self.parts).finish()
634    }
635}
636
637#[derive(UpmapFromRaFixture)]
638pub struct InlayHintLabelPart {
639    pub text: String,
640    /// Source location represented by this label part. The client will use this to fetch the part's
641    /// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location
642    /// refers to (not necessarily the location itself).
643    /// When setting this, no tooltip must be set on the containing hint, or VS Code will display
644    /// them both.
645    pub linked_location: Option<LazyProperty<FileRange>>,
646    /// The tooltip to show when hovering over the inlay hint, this may invoke other actions like
647    /// hover requests to show.
648    pub tooltip: Option<LazyProperty<InlayTooltip>>,
649}
650
651impl std::hash::Hash for InlayHintLabelPart {
652    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
653        self.text.hash(state);
654        self.linked_location.is_some().hash(state);
655        self.tooltip.is_some().hash(state);
656    }
657}
658
659impl fmt::Debug for InlayHintLabelPart {
660    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661        match self {
662            Self { text, linked_location: None, tooltip: None | Some(LazyProperty::Lazy) } => {
663                text.fmt(f)
664            }
665            Self { text, linked_location, tooltip } => f
666                .debug_struct("InlayHintLabelPart")
667                .field("text", text)
668                .field("linked_location", linked_location)
669                .field(
670                    "tooltip",
671                    &tooltip.as_ref().map_or("", |it| match it {
672                        LazyProperty::Computed(
673                            InlayTooltip::String(it) | InlayTooltip::Markdown(it),
674                        ) => it,
675                        LazyProperty::Lazy => "",
676                    }),
677                )
678                .finish(),
679        }
680    }
681}
682
683#[derive(Debug)]
684struct InlayHintLabelBuilder<'a> {
685    sema: &'a Semantics<'a, RootDatabase>,
686    result: InlayHintLabel,
687    last_part: String,
688    resolve: bool,
689    location: Option<LazyProperty<FileRange>>,
690}
691
692impl fmt::Write for InlayHintLabelBuilder<'_> {
693    fn write_str(&mut self, s: &str) -> fmt::Result {
694        self.last_part.write_str(s)
695    }
696}
697
698impl HirWrite for InlayHintLabelBuilder<'_> {
699    fn start_location_link(&mut self, def: ModuleDefId) {
700        never!(self.location.is_some(), "location link is already started");
701        self.make_new_part();
702
703        self.location = Some(if self.resolve {
704            LazyProperty::Lazy
705        } else {
706            LazyProperty::Computed({
707                let Some(location) = ModuleDef::from(def).try_to_nav(self.sema) else { return };
708                let location = location.call_site();
709                FileRange { file_id: location.file_id, range: location.focus_or_full_range() }
710            })
711        });
712    }
713
714    fn end_location_link(&mut self) {
715        self.make_new_part();
716    }
717}
718
719impl InlayHintLabelBuilder<'_> {
720    fn make_new_part(&mut self) {
721        let text = take(&mut self.last_part);
722        if !text.is_empty() {
723            self.result.parts.push(InlayHintLabelPart {
724                text,
725                linked_location: self.location.take(),
726                tooltip: None,
727            });
728        }
729    }
730
731    fn finish(mut self) -> InlayHintLabel {
732        self.make_new_part();
733        self.result
734    }
735}
736
737fn label_of_ty(
738    famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
739    config: &InlayHintsConfig<'_>,
740    ty: &hir::Type<'_>,
741    display_target: DisplayTarget,
742) -> Option<InlayHintLabel> {
743    fn rec(
744        sema: &Semantics<'_, RootDatabase>,
745        famous_defs: &FamousDefs<'_, '_>,
746        mut max_length: Option<usize>,
747        ty: &hir::Type<'_>,
748        label_builder: &mut InlayHintLabelBuilder<'_>,
749        config: &InlayHintsConfig<'_>,
750        display_target: DisplayTarget,
751    ) -> Result<(), HirDisplayError> {
752        hir::attach_db(sema.db, || {
753            let iter_item_type = hint_iterator(sema, famous_defs, ty);
754            match iter_item_type {
755                Some((iter_trait, item, ty)) => {
756                    const LABEL_START: &str = "impl ";
757                    const LABEL_ITERATOR: &str = "Iterator";
758                    const LABEL_MIDDLE: &str = "<";
759                    const LABEL_ITEM: &str = "Item";
760                    const LABEL_MIDDLE2: &str = " = ";
761                    const LABEL_END: &str = ">";
762
763                    max_length = max_length.map(|len| {
764                        len.saturating_sub(
765                            LABEL_START.len()
766                                + LABEL_ITERATOR.len()
767                                + LABEL_MIDDLE.len()
768                                + LABEL_MIDDLE2.len()
769                                + LABEL_END.len(),
770                        )
771                    });
772
773                    label_builder.write_str(LABEL_START)?;
774                    label_builder.start_location_link(ModuleDef::from(iter_trait).into());
775                    label_builder.write_str(LABEL_ITERATOR)?;
776                    label_builder.end_location_link();
777                    label_builder.write_str(LABEL_MIDDLE)?;
778                    label_builder.start_location_link(ModuleDef::from(item).into());
779                    label_builder.write_str(LABEL_ITEM)?;
780                    label_builder.end_location_link();
781                    label_builder.write_str(LABEL_MIDDLE2)?;
782                    rec(sema, famous_defs, max_length, &ty, label_builder, config, display_target)?;
783                    label_builder.write_str(LABEL_END)?;
784                    Ok(())
785                }
786                None => ty
787                    .display_truncated(sema.db, max_length, display_target)
788                    .with_closure_style(config.closure_style)
789                    .write_to(label_builder),
790            }
791        })
792    }
793
794    let mut label_builder = InlayHintLabelBuilder {
795        sema,
796        last_part: String::new(),
797        location: None,
798        result: InlayHintLabel::default(),
799        resolve: config.fields_to_resolve.resolve_label_location,
800    };
801    let _ =
802        rec(sema, famous_defs, config.max_length, ty, &mut label_builder, config, display_target);
803    let r = label_builder.finish();
804    Some(r)
805}
806
807/// Checks if the type is an Iterator from std::iter and returns the iterator trait and the item type of the concrete iterator.
808fn hint_iterator<'db>(
809    sema: &Semantics<'db, RootDatabase>,
810    famous_defs: &FamousDefs<'_, 'db>,
811    ty: &hir::Type<'db>,
812) -> Option<(hir::Trait, hir::TypeAlias, hir::Type<'db>)> {
813    let db = sema.db;
814    let strukt = ty.strip_references().as_adt()?;
815    let krate = strukt.module(db).krate(db);
816    if krate != famous_defs.core()? {
817        return None;
818    }
819    let iter_trait = famous_defs.core_iter_Iterator()?;
820    let iter_mod = famous_defs.core_iter()?;
821
822    // Assert that this struct comes from `core::iter`.
823    if !(strukt.visibility(db) == hir::Visibility::Public
824        && strukt.module(db).path_to_root(db).contains(&iter_mod))
825    {
826        return None;
827    }
828
829    if ty.impls_trait(db, iter_trait, &[]) {
830        let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item {
831            hir::AssocItem::TypeAlias(alias) if alias.name(db) == sym::Item => Some(alias),
832            _ => None,
833        })?;
834        if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) {
835            return Some((iter_trait, assoc_type_item, ty));
836        }
837    }
838
839    None
840}
841
842fn ty_to_text_edit(
843    sema: &Semantics<'_, RootDatabase>,
844    config: &InlayHintsConfig<'_>,
845    node_for_hint: &SyntaxNode,
846    ty: &hir::Type<'_>,
847    offset_to_insert_ty: TextSize,
848    additional_edits: &dyn Fn(&mut TextEditBuilder),
849    prefix: impl Into<String>,
850) -> Option<LazyProperty<TextEdit>> {
851    // FIXME: Limit the length and bail out on excess somehow?
852    let rendered = sema
853        .scope(node_for_hint)
854        .and_then(|scope| ty.display_source_code(scope.db, scope.module().into(), false).ok())?;
855    Some(config.lazy_text_edit(|| {
856        let mut builder = TextEdit::builder();
857        builder.insert(offset_to_insert_ty, prefix.into());
858        builder.insert(offset_to_insert_ty, rendered);
859
860        additional_edits(&mut builder);
861
862        builder.finish()
863    }))
864}
865
866fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
867    matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
868}
869
870#[cfg(test)]
871mod tests {
872
873    use expect_test::Expect;
874    use hir::ClosureStyle;
875    use ide_db::MiniCore;
876    use itertools::Itertools;
877    use test_utils::extract_annotations;
878
879    use crate::DiscriminantHints;
880    use crate::inlay_hints::{AdjustmentHints, AdjustmentHintsMode};
881    use crate::{LifetimeElisionHints, fixture, inlay_hints::InlayHintsConfig};
882
883    use super::{ClosureReturnTypeHints, GenericParameterHints, InlayFieldsToResolve};
884
885    pub(super) const DISABLED_CONFIG: InlayHintsConfig<'_> = InlayHintsConfig {
886        discriminant_hints: DiscriminantHints::Never,
887        render_colons: false,
888        type_hints: false,
889        parameter_hints: false,
890        parameter_hints_for_missing_arguments: false,
891        sized_bound: false,
892        generic_parameter_hints: GenericParameterHints {
893            type_hints: false,
894            lifetime_hints: false,
895            const_hints: false,
896        },
897        chaining_hints: false,
898        lifetime_elision_hints: LifetimeElisionHints::Never,
899        closure_return_type_hints: ClosureReturnTypeHints::Never,
900        closure_capture_hints: false,
901        adjustment_hints: AdjustmentHints::Never,
902        adjustment_hints_disable_reborrows: false,
903        adjustment_hints_mode: AdjustmentHintsMode::Prefix,
904        adjustment_hints_hide_outside_unsafe: false,
905        binding_mode_hints: false,
906        hide_inferred_type_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}