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