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