ra_ap_ide/
inlay_hints.rs

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