• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use std::{
2     fmt::{self, Write},
3     mem::take,
4 };
5 
6 use either::Either;
7 use hir::{
8     known, ClosureStyle, HasVisibility, HirDisplay, HirDisplayError, HirWrite, ModuleDef,
9     ModuleDefId, Semantics,
10 };
11 use ide_db::{base_db::FileRange, famous_defs::FamousDefs, RootDatabase};
12 use itertools::Itertools;
13 use smallvec::{smallvec, SmallVec};
14 use stdx::never;
15 use syntax::{
16     ast::{self, AstNode},
17     match_ast, NodeOrToken, SyntaxNode, TextRange, TextSize,
18 };
19 use text_edit::TextEdit;
20 
21 use crate::{navigation_target::TryToNav, FileId};
22 
23 mod adjustment;
24 mod bind_pat;
25 mod binding_mode;
26 mod chaining;
27 mod closing_brace;
28 mod closure_ret;
29 mod closure_captures;
30 mod discriminant;
31 mod fn_lifetime_fn;
32 mod implicit_static;
33 mod param_name;
34 
35 #[derive(Clone, Debug, PartialEq, Eq)]
36 pub struct InlayHintsConfig {
37     pub render_colons: bool,
38     pub type_hints: bool,
39     pub discriminant_hints: DiscriminantHints,
40     pub parameter_hints: bool,
41     pub chaining_hints: bool,
42     pub adjustment_hints: AdjustmentHints,
43     pub adjustment_hints_mode: AdjustmentHintsMode,
44     pub adjustment_hints_hide_outside_unsafe: bool,
45     pub closure_return_type_hints: ClosureReturnTypeHints,
46     pub closure_capture_hints: bool,
47     pub binding_mode_hints: bool,
48     pub lifetime_elision_hints: LifetimeElisionHints,
49     pub param_names_for_lifetime_elision_hints: bool,
50     pub hide_named_constructor_hints: bool,
51     pub hide_closure_initialization_hints: bool,
52     pub closure_style: ClosureStyle,
53     pub max_length: Option<usize>,
54     pub closing_brace_hints_min_lines: Option<usize>,
55 }
56 
57 #[derive(Clone, Debug, PartialEq, Eq)]
58 pub enum ClosureReturnTypeHints {
59     Always,
60     WithBlock,
61     Never,
62 }
63 
64 #[derive(Clone, Debug, PartialEq, Eq)]
65 pub enum DiscriminantHints {
66     Always,
67     Never,
68     Fieldless,
69 }
70 
71 #[derive(Clone, Debug, PartialEq, Eq)]
72 pub enum LifetimeElisionHints {
73     Always,
74     SkipTrivial,
75     Never,
76 }
77 
78 #[derive(Clone, Debug, PartialEq, Eq)]
79 pub enum AdjustmentHints {
80     Always,
81     ReborrowOnly,
82     Never,
83 }
84 
85 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
86 pub enum AdjustmentHintsMode {
87     Prefix,
88     Postfix,
89     PreferPrefix,
90     PreferPostfix,
91 }
92 
93 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
94 pub enum InlayKind {
95     Adjustment,
96     BindingMode,
97     Chaining,
98     ClosingBrace,
99     ClosureCapture,
100     Discriminant,
101     GenericParamList,
102     Lifetime,
103     Parameter,
104     Type,
105 }
106 
107 #[derive(Debug)]
108 pub enum InlayHintPosition {
109     Before,
110     After,
111 }
112 
113 #[derive(Debug)]
114 pub struct InlayHint {
115     /// The text range this inlay hint applies to.
116     pub range: TextRange,
117     pub position: InlayHintPosition,
118     pub pad_left: bool,
119     pub pad_right: bool,
120     /// The kind of this inlay hint.
121     pub kind: InlayKind,
122     /// The actual label to show in the inlay hint.
123     pub label: InlayHintLabel,
124     /// Text edit to apply when "accepting" this inlay hint.
125     pub text_edit: Option<TextEdit>,
126 }
127 
128 impl InlayHint {
closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint129     fn closing_paren_after(kind: InlayKind, range: TextRange) -> InlayHint {
130         InlayHint {
131             range,
132             kind,
133             label: InlayHintLabel::from(")"),
134             text_edit: None,
135             position: InlayHintPosition::After,
136             pad_left: false,
137             pad_right: false,
138         }
139     }
opening_paren_before(kind: InlayKind, range: TextRange) -> InlayHint140     fn opening_paren_before(kind: InlayKind, range: TextRange) -> InlayHint {
141         InlayHint {
142             range,
143             kind,
144             label: InlayHintLabel::from("("),
145             text_edit: None,
146             position: InlayHintPosition::Before,
147             pad_left: false,
148             pad_right: false,
149         }
150     }
151 }
152 
153 #[derive(Debug)]
154 pub enum InlayTooltip {
155     String(String),
156     Markdown(String),
157 }
158 
159 #[derive(Default)]
160 pub struct InlayHintLabel {
161     pub parts: SmallVec<[InlayHintLabelPart; 1]>,
162 }
163 
164 impl InlayHintLabel {
simple( s: impl Into<String>, tooltip: Option<InlayTooltip>, linked_location: Option<FileRange>, ) -> InlayHintLabel165     pub fn simple(
166         s: impl Into<String>,
167         tooltip: Option<InlayTooltip>,
168         linked_location: Option<FileRange>,
169     ) -> InlayHintLabel {
170         InlayHintLabel {
171             parts: smallvec![InlayHintLabelPart { text: s.into(), linked_location, tooltip }],
172         }
173     }
174 
prepend_str(&mut self, s: &str)175     pub fn prepend_str(&mut self, s: &str) {
176         match &mut *self.parts {
177             [InlayHintLabelPart { text, linked_location: None, tooltip: None }, ..] => {
178                 text.insert_str(0, s)
179             }
180             _ => self.parts.insert(
181                 0,
182                 InlayHintLabelPart { text: s.into(), linked_location: None, tooltip: None },
183             ),
184         }
185     }
186 
append_str(&mut self, s: &str)187     pub fn append_str(&mut self, s: &str) {
188         match &mut *self.parts {
189             [.., InlayHintLabelPart { text, linked_location: None, tooltip: None }] => {
190                 text.push_str(s)
191             }
192             _ => self.parts.push(InlayHintLabelPart {
193                 text: s.into(),
194                 linked_location: None,
195                 tooltip: None,
196             }),
197         }
198     }
199 }
200 
201 impl From<String> for InlayHintLabel {
from(s: String) -> Self202     fn from(s: String) -> Self {
203         Self {
204             parts: smallvec![InlayHintLabelPart { text: s, linked_location: None, tooltip: None }],
205         }
206     }
207 }
208 
209 impl From<&str> for InlayHintLabel {
from(s: &str) -> Self210     fn from(s: &str) -> Self {
211         Self {
212             parts: smallvec![InlayHintLabelPart {
213                 text: s.into(),
214                 linked_location: None,
215                 tooltip: None
216             }],
217         }
218     }
219 }
220 
221 impl fmt::Display for InlayHintLabel {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result222     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223         write!(f, "{}", self.parts.iter().map(|part| &part.text).format(""))
224     }
225 }
226 
227 impl fmt::Debug for InlayHintLabel {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result228     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229         f.debug_list().entries(&self.parts).finish()
230     }
231 }
232 
233 pub struct InlayHintLabelPart {
234     pub text: String,
235     /// Source location represented by this label part. The client will use this to fetch the part's
236     /// hover tooltip, and Ctrl+Clicking the label part will navigate to the definition the location
237     /// refers to (not necessarily the location itself).
238     /// When setting this, no tooltip must be set on the containing hint, or VS Code will display
239     /// them both.
240     pub linked_location: Option<FileRange>,
241     /// The tooltip to show when hovering over the inlay hint, this may invoke other actions like
242     /// hover requests to show.
243     pub tooltip: Option<InlayTooltip>,
244 }
245 
246 impl fmt::Debug for InlayHintLabelPart {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result247     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248         match self {
249             Self { text, linked_location: None, tooltip: None } => text.fmt(f),
250             Self { text, linked_location, tooltip } => f
251                 .debug_struct("InlayHintLabelPart")
252                 .field("text", text)
253                 .field("linked_location", linked_location)
254                 .field(
255                     "tooltip",
256                     &tooltip.as_ref().map_or("", |it| match it {
257                         InlayTooltip::String(it) | InlayTooltip::Markdown(it) => it,
258                     }),
259                 )
260                 .finish(),
261         }
262     }
263 }
264 
265 #[derive(Debug)]
266 struct InlayHintLabelBuilder<'a> {
267     db: &'a RootDatabase,
268     result: InlayHintLabel,
269     last_part: String,
270     location: Option<FileRange>,
271 }
272 
273 impl fmt::Write for InlayHintLabelBuilder<'_> {
write_str(&mut self, s: &str) -> fmt::Result274     fn write_str(&mut self, s: &str) -> fmt::Result {
275         self.last_part.write_str(s)
276     }
277 }
278 
279 impl HirWrite for InlayHintLabelBuilder<'_> {
start_location_link(&mut self, def: ModuleDefId)280     fn start_location_link(&mut self, def: ModuleDefId) {
281         if self.location.is_some() {
282             never!("location link is already started");
283         }
284         self.make_new_part();
285         let Some(location) = ModuleDef::from(def).try_to_nav(self.db) else { return };
286         let location =
287             FileRange { file_id: location.file_id, range: location.focus_or_full_range() };
288         self.location = Some(location);
289     }
290 
end_location_link(&mut self)291     fn end_location_link(&mut self) {
292         self.make_new_part();
293     }
294 }
295 
296 impl InlayHintLabelBuilder<'_> {
make_new_part(&mut self)297     fn make_new_part(&mut self) {
298         self.result.parts.push(InlayHintLabelPart {
299             text: take(&mut self.last_part),
300             linked_location: self.location.take(),
301             tooltip: None,
302         });
303     }
304 
finish(mut self) -> InlayHintLabel305     fn finish(mut self) -> InlayHintLabel {
306         self.make_new_part();
307         self.result
308     }
309 }
310 
label_of_ty( famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig, ty: &hir::Type, ) -> Option<InlayHintLabel>311 fn label_of_ty(
312     famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
313     config: &InlayHintsConfig,
314     ty: &hir::Type,
315 ) -> Option<InlayHintLabel> {
316     fn rec(
317         sema: &Semantics<'_, RootDatabase>,
318         famous_defs: &FamousDefs<'_, '_>,
319         mut max_length: Option<usize>,
320         ty: &hir::Type,
321         label_builder: &mut InlayHintLabelBuilder<'_>,
322         config: &InlayHintsConfig,
323     ) -> Result<(), HirDisplayError> {
324         let iter_item_type = hint_iterator(sema, famous_defs, &ty);
325         match iter_item_type {
326             Some((iter_trait, item, ty)) => {
327                 const LABEL_START: &str = "impl ";
328                 const LABEL_ITERATOR: &str = "Iterator";
329                 const LABEL_MIDDLE: &str = "<";
330                 const LABEL_ITEM: &str = "Item";
331                 const LABEL_MIDDLE2: &str = " = ";
332                 const LABEL_END: &str = ">";
333 
334                 max_length = max_length.map(|len| {
335                     len.saturating_sub(
336                         LABEL_START.len()
337                             + LABEL_ITERATOR.len()
338                             + LABEL_MIDDLE.len()
339                             + LABEL_MIDDLE2.len()
340                             + LABEL_END.len(),
341                     )
342                 });
343 
344                 label_builder.write_str(LABEL_START)?;
345                 label_builder.start_location_link(ModuleDef::from(iter_trait).into());
346                 label_builder.write_str(LABEL_ITERATOR)?;
347                 label_builder.end_location_link();
348                 label_builder.write_str(LABEL_MIDDLE)?;
349                 label_builder.start_location_link(ModuleDef::from(item).into());
350                 label_builder.write_str(LABEL_ITEM)?;
351                 label_builder.end_location_link();
352                 label_builder.write_str(LABEL_MIDDLE2)?;
353                 rec(sema, famous_defs, max_length, &ty, label_builder, config)?;
354                 label_builder.write_str(LABEL_END)?;
355                 Ok(())
356             }
357             None => ty
358                 .display_truncated(sema.db, max_length)
359                 .with_closure_style(config.closure_style)
360                 .write_to(label_builder),
361         }
362     }
363 
364     let mut label_builder = InlayHintLabelBuilder {
365         db: sema.db,
366         last_part: String::new(),
367         location: None,
368         result: InlayHintLabel::default(),
369     };
370     let _ = rec(sema, famous_defs, config.max_length, ty, &mut label_builder, config);
371     let r = label_builder.finish();
372     Some(r)
373 }
374 
ty_to_text_edit( sema: &Semantics<'_, RootDatabase>, node_for_hint: &SyntaxNode, ty: &hir::Type, offset_to_insert: TextSize, prefix: String, ) -> Option<TextEdit>375 fn ty_to_text_edit(
376     sema: &Semantics<'_, RootDatabase>,
377     node_for_hint: &SyntaxNode,
378     ty: &hir::Type,
379     offset_to_insert: TextSize,
380     prefix: String,
381 ) -> Option<TextEdit> {
382     let scope = sema.scope(node_for_hint)?;
383     // FIXME: Limit the length and bail out on excess somehow?
384     let rendered = ty.display_source_code(scope.db, scope.module().into(), false).ok()?;
385 
386     let mut builder = TextEdit::builder();
387     builder.insert(offset_to_insert, prefix);
388     builder.insert(offset_to_insert, rendered);
389     Some(builder.finish())
390 }
391 
392 // Feature: Inlay Hints
393 //
394 // rust-analyzer shows additional information inline with the source code.
395 // Editors usually render this using read-only virtual text snippets interspersed with code.
396 //
397 // rust-analyzer by default shows hints for
398 //
399 // * types of local variables
400 // * names of function arguments
401 // * types of chained expressions
402 //
403 // Optionally, one can enable additional hints for
404 //
405 // * return types of closure expressions
406 // * elided lifetimes
407 // * compiler inserted reborrows
408 //
409 // image::https://user-images.githubusercontent.com/48062697/113020660-b5f98b80-917a-11eb-8d70-3be3fd558cdd.png[]
inlay_hints( db: &RootDatabase, file_id: FileId, range_limit: Option<TextRange>, config: &InlayHintsConfig, ) -> Vec<InlayHint>410 pub(crate) fn inlay_hints(
411     db: &RootDatabase,
412     file_id: FileId,
413     range_limit: Option<TextRange>,
414     config: &InlayHintsConfig,
415 ) -> Vec<InlayHint> {
416     let _p = profile::span("inlay_hints");
417     let sema = Semantics::new(db);
418     let file = sema.parse(file_id);
419     let file = file.syntax();
420 
421     let mut acc = Vec::new();
422 
423     if let Some(scope) = sema.scope(file) {
424         let famous_defs = FamousDefs(&sema, scope.krate());
425 
426         let hints = |node| hints(&mut acc, &famous_defs, config, file_id, node);
427         match range_limit {
428             Some(range) => match file.covering_element(range) {
429                 NodeOrToken::Token(_) => return acc,
430                 NodeOrToken::Node(n) => n
431                     .descendants()
432                     .filter(|descendant| range.intersect(descendant.text_range()).is_some())
433                     .for_each(hints),
434             },
435             None => file.descendants().for_each(hints),
436         };
437     }
438 
439     acc
440 }
441 
hints( hints: &mut Vec<InlayHint>, famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>, config: &InlayHintsConfig, file_id: FileId, node: SyntaxNode, )442 fn hints(
443     hints: &mut Vec<InlayHint>,
444     famous_defs @ FamousDefs(sema, _): &FamousDefs<'_, '_>,
445     config: &InlayHintsConfig,
446     file_id: FileId,
447     node: SyntaxNode,
448 ) {
449     closing_brace::hints(hints, sema, config, file_id, node.clone());
450     match_ast! {
451         match node {
452             ast::Expr(expr) => {
453                 chaining::hints(hints, famous_defs, config, file_id, &expr);
454                 adjustment::hints(hints, sema, config, &expr);
455                 match expr {
456                     ast::Expr::CallExpr(it) => param_name::hints(hints, sema, config, ast::Expr::from(it)),
457                     ast::Expr::MethodCallExpr(it) => {
458                         param_name::hints(hints, sema, config, ast::Expr::from(it))
459                     }
460                     ast::Expr::ClosureExpr(it) => {
461                         closure_captures::hints(hints, famous_defs, config, file_id, it.clone());
462                         closure_ret::hints(hints, famous_defs, config, file_id, it)
463                     },
464                     _ => None,
465                 }
466             },
467             ast::Pat(it) => {
468                 binding_mode::hints(hints, sema, config, &it);
469                 if let ast::Pat::IdentPat(it) = it {
470                     bind_pat::hints(hints, famous_defs, config, file_id, &it);
471                 }
472                 Some(())
473             },
474             ast::Item(it) => match it {
475                 // FIXME: record impl lifetimes so they aren't being reused in assoc item lifetime inlay hints
476                 ast::Item::Impl(_) => None,
477                 ast::Item::Fn(it) => fn_lifetime_fn::hints(hints, config, it),
478                 // static type elisions
479                 ast::Item::Static(it) => implicit_static::hints(hints, config, Either::Left(it)),
480                 ast::Item::Const(it) => implicit_static::hints(hints, config, Either::Right(it)),
481                 ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, file_id, it),
482                 _ => None,
483             },
484             // FIXME: fn-ptr type, dyn fn type, and trait object type elisions
485             ast::Type(_) => None,
486             _ => None,
487         }
488     };
489 }
490 
491 /// Checks if the type is an Iterator from std::iter and returns the iterator trait and the item type of the concrete iterator.
hint_iterator( sema: &Semantics<'_, RootDatabase>, famous_defs: &FamousDefs<'_, '_>, ty: &hir::Type, ) -> Option<(hir::Trait, hir::TypeAlias, hir::Type)>492 fn hint_iterator(
493     sema: &Semantics<'_, RootDatabase>,
494     famous_defs: &FamousDefs<'_, '_>,
495     ty: &hir::Type,
496 ) -> Option<(hir::Trait, hir::TypeAlias, hir::Type)> {
497     let db = sema.db;
498     let strukt = ty.strip_references().as_adt()?;
499     let krate = strukt.module(db).krate();
500     if krate != famous_defs.core()? {
501         return None;
502     }
503     let iter_trait = famous_defs.core_iter_Iterator()?;
504     let iter_mod = famous_defs.core_iter()?;
505 
506     // Assert that this struct comes from `core::iter`.
507     if !(strukt.visibility(db) == hir::Visibility::Public
508         && strukt.module(db).path_to_root(db).contains(&iter_mod))
509     {
510         return None;
511     }
512 
513     if ty.impls_trait(db, iter_trait, &[]) {
514         let assoc_type_item = iter_trait.items(db).into_iter().find_map(|item| match item {
515             hir::AssocItem::TypeAlias(alias) if alias.name(db) == known::Item => Some(alias),
516             _ => None,
517         })?;
518         if let Some(ty) = ty.normalize_trait_assoc_type(db, &[], assoc_type_item) {
519             return Some((iter_trait, assoc_type_item, ty));
520         }
521     }
522 
523     None
524 }
525 
closure_has_block_body(closure: &ast::ClosureExpr) -> bool526 fn closure_has_block_body(closure: &ast::ClosureExpr) -> bool {
527     matches!(closure.body(), Some(ast::Expr::BlockExpr(_)))
528 }
529 
530 #[cfg(test)]
531 mod tests {
532     use expect_test::Expect;
533     use hir::ClosureStyle;
534     use itertools::Itertools;
535     use test_utils::extract_annotations;
536 
537     use crate::inlay_hints::{AdjustmentHints, AdjustmentHintsMode};
538     use crate::DiscriminantHints;
539     use crate::{fixture, inlay_hints::InlayHintsConfig, LifetimeElisionHints};
540 
541     use super::ClosureReturnTypeHints;
542 
543     pub(super) const DISABLED_CONFIG: InlayHintsConfig = InlayHintsConfig {
544         discriminant_hints: DiscriminantHints::Never,
545         render_colons: false,
546         type_hints: false,
547         parameter_hints: false,
548         chaining_hints: false,
549         lifetime_elision_hints: LifetimeElisionHints::Never,
550         closure_return_type_hints: ClosureReturnTypeHints::Never,
551         closure_capture_hints: false,
552         adjustment_hints: AdjustmentHints::Never,
553         adjustment_hints_mode: AdjustmentHintsMode::Prefix,
554         adjustment_hints_hide_outside_unsafe: false,
555         binding_mode_hints: false,
556         hide_named_constructor_hints: false,
557         hide_closure_initialization_hints: false,
558         closure_style: ClosureStyle::ImplFn,
559         param_names_for_lifetime_elision_hints: false,
560         max_length: None,
561         closing_brace_hints_min_lines: None,
562     };
563     pub(super) const TEST_CONFIG: InlayHintsConfig = InlayHintsConfig {
564         type_hints: true,
565         parameter_hints: true,
566         chaining_hints: true,
567         closure_return_type_hints: ClosureReturnTypeHints::WithBlock,
568         binding_mode_hints: true,
569         lifetime_elision_hints: LifetimeElisionHints::Always,
570         ..DISABLED_CONFIG
571     };
572 
573     #[track_caller]
check(ra_fixture: &str)574     pub(super) fn check(ra_fixture: &str) {
575         check_with_config(TEST_CONFIG, ra_fixture);
576     }
577 
578     #[track_caller]
check_with_config(config: InlayHintsConfig, ra_fixture: &str)579     pub(super) fn check_with_config(config: InlayHintsConfig, ra_fixture: &str) {
580         let (analysis, file_id) = fixture::file(ra_fixture);
581         let mut expected = extract_annotations(&analysis.file_text(file_id).unwrap());
582         let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
583         let actual = inlay_hints
584             .into_iter()
585             // FIXME: We trim the start because some inlay produces leading whitespace which is not properly supported by our annotation extraction
586             .map(|it| (it.range, it.label.to_string().trim_start().to_owned()))
587             .sorted_by_key(|(range, _)| range.start())
588             .collect::<Vec<_>>();
589         expected.sort_by_key(|(range, _)| range.start());
590 
591         assert_eq!(expected, actual, "\nExpected:\n{expected:#?}\n\nActual:\n{actual:#?}");
592     }
593 
594     #[track_caller]
check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect)595     pub(super) fn check_expect(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
596         let (analysis, file_id) = fixture::file(ra_fixture);
597         let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
598         expect.assert_debug_eq(&inlay_hints)
599     }
600 
601     /// Computes inlay hints for the fixture, applies all the provided text edits and then runs
602     /// expect test.
603     #[track_caller]
check_edit(config: InlayHintsConfig, ra_fixture: &str, expect: Expect)604     pub(super) fn check_edit(config: InlayHintsConfig, ra_fixture: &str, expect: Expect) {
605         let (analysis, file_id) = fixture::file(ra_fixture);
606         let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
607 
608         let edits = inlay_hints
609             .into_iter()
610             .filter_map(|hint| hint.text_edit)
611             .reduce(|mut acc, next| {
612                 acc.union(next).expect("merging text edits failed");
613                 acc
614             })
615             .expect("no edit returned");
616 
617         let mut actual = analysis.file_text(file_id).unwrap().to_string();
618         edits.apply(&mut actual);
619         expect.assert_eq(&actual);
620     }
621 
622     #[track_caller]
check_no_edit(config: InlayHintsConfig, ra_fixture: &str)623     pub(super) fn check_no_edit(config: InlayHintsConfig, ra_fixture: &str) {
624         let (analysis, file_id) = fixture::file(ra_fixture);
625         let inlay_hints = analysis.inlay_hints(&config, file_id, None).unwrap();
626 
627         let edits: Vec<_> = inlay_hints.into_iter().filter_map(|hint| hint.text_edit).collect();
628 
629         assert!(edits.is_empty(), "unexpected edits: {edits:?}");
630     }
631 
632     #[test]
hints_disabled()633     fn hints_disabled() {
634         check_with_config(
635             InlayHintsConfig { render_colons: true, ..DISABLED_CONFIG },
636             r#"
637 fn foo(a: i32, b: i32) -> i32 { a + b }
638 fn main() {
639     let _x = foo(4, 4);
640 }"#,
641         );
642     }
643 }
644