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