Deduplicate inlays when getting those from multiple language servers (#42899)

Kirill Bulatov created

Part of https://github.com/zed-industries/zed/issues/42671

Release Notes:

- Deduplicate inlay hints from different language servers

Change summary

crates/editor/src/inlays/inlay_hints.rs | 118 ++++++++++++++++++++++++++
1 file changed, 115 insertions(+), 3 deletions(-)

Detailed changes

crates/editor/src/inlays/inlay_hints.rs 🔗

@@ -1,4 +1,5 @@
 use std::{
+    collections::hash_map,
     ops::{ControlFlow, Range},
     time::Duration,
 };
@@ -778,6 +779,7 @@ impl Editor {
         }
 
         let excerpts = self.buffer.read(cx).excerpt_ids();
+        let mut inserted_hint_text = HashMap::default();
         let hints_to_insert = new_hints
             .into_iter()
             .filter_map(|(chunk_range, hints_result)| {
@@ -804,8 +806,35 @@ impl Editor {
                     }
                 }
             })
-            .flat_map(|hints| hints.into_values())
-            .flatten()
+            .flat_map(|new_hints| {
+                let mut hints_deduplicated = Vec::new();
+
+                if new_hints.len() > 1 {
+                    for (server_id, new_hints) in new_hints {
+                        for (new_id, new_hint) in new_hints {
+                            let hints_text_for_position = inserted_hint_text
+                                .entry(new_hint.position)
+                                .or_insert_with(HashMap::default);
+                            let insert =
+                                match hints_text_for_position.entry(new_hint.text().to_string()) {
+                                    hash_map::Entry::Occupied(o) => o.get() == &server_id,
+                                    hash_map::Entry::Vacant(v) => {
+                                        v.insert(server_id);
+                                        true
+                                    }
+                                };
+
+                            if insert {
+                                hints_deduplicated.push((new_id, new_hint));
+                            }
+                        }
+                    }
+                } else {
+                    hints_deduplicated.extend(new_hints.into_values().flatten());
+                }
+
+                hints_deduplicated
+            })
             .filter_map(|(hint_id, lsp_hint)| {
                 if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
                     && inlay_hints
@@ -3732,6 +3761,7 @@ let c = 3;"#
         let mut fake_servers = language_registry.register_fake_lsp(
             "Rust",
             FakeLspAdapter {
+                name: "rust-analyzer",
                 capabilities: lsp::ServerCapabilities {
                     inlay_hint_provider: Some(lsp::OneOf::Left(true)),
                     ..lsp::ServerCapabilities::default()
@@ -3804,6 +3834,78 @@ let c = 3;"#
             },
         );
 
+        // Add another server that does send the same, duplicate hints back
+        let mut fake_servers_2 = language_registry.register_fake_lsp(
+            "Rust",
+            FakeLspAdapter {
+                name: "CrabLang-ls",
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..lsp::ServerCapabilities::default()
+                },
+                initializer: Some(Box::new(move |fake_server| {
+                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
+                        move |params, _| async move {
+                            if params.text_document.uri
+                                == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
+                            {
+                                Ok(Some(vec![
+                                    lsp::InlayHint {
+                                        position: lsp::Position::new(1, 9),
+                                        label: lsp::InlayHintLabel::String(": i32".to_owned()),
+                                        kind: Some(lsp::InlayHintKind::TYPE),
+                                        text_edits: None,
+                                        tooltip: None,
+                                        padding_left: None,
+                                        padding_right: None,
+                                        data: None,
+                                    },
+                                    lsp::InlayHint {
+                                        position: lsp::Position::new(19, 9),
+                                        label: lsp::InlayHintLabel::String(": i33".to_owned()),
+                                        kind: Some(lsp::InlayHintKind::TYPE),
+                                        text_edits: None,
+                                        tooltip: None,
+                                        padding_left: None,
+                                        padding_right: None,
+                                        data: None,
+                                    },
+                                ]))
+                            } else if params.text_document.uri
+                                == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
+                            {
+                                Ok(Some(vec![
+                                    lsp::InlayHint {
+                                        position: lsp::Position::new(1, 10),
+                                        label: lsp::InlayHintLabel::String(": i34".to_owned()),
+                                        kind: Some(lsp::InlayHintKind::TYPE),
+                                        text_edits: None,
+                                        tooltip: None,
+                                        padding_left: None,
+                                        padding_right: None,
+                                        data: None,
+                                    },
+                                    lsp::InlayHint {
+                                        position: lsp::Position::new(29, 10),
+                                        label: lsp::InlayHintLabel::String(": i35".to_owned()),
+                                        kind: Some(lsp::InlayHintKind::TYPE),
+                                        text_edits: None,
+                                        tooltip: None,
+                                        padding_left: None,
+                                        padding_right: None,
+                                        data: None,
+                                    },
+                                ]))
+                            } else {
+                                panic!("Unexpected file path {:?}", params.text_document.uri);
+                            }
+                        },
+                    );
+                })),
+                ..FakeLspAdapter::default()
+            },
+        );
+
         let (buffer_1, _handle_1) = project
             .update(cx, |project, cx| {
                 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
@@ -3847,6 +3949,7 @@ let c = 3;"#
         });
 
         let fake_server = fake_servers.next().await.unwrap();
+        let _fake_server_2 = fake_servers_2.next().await.unwrap();
         cx.executor().advance_clock(Duration::from_millis(100));
         cx.executor().run_until_parked();
 
@@ -3854,12 +3957,17 @@ let c = 3;"#
             .update(cx, |editor, _window, cx| {
                 assert_eq!(
                     vec![
+                        ": i32".to_string(),
                         ": i32".to_string(),
                         ": i33".to_string(),
+                        ": i33".to_string(),
+                        ": i34".to_string(),
                         ": i34".to_string(),
                         ": i35".to_string(),
+                        ": i35".to_string(),
                     ],
                     sorted_cached_hint_labels(editor, cx),
+                    "We receive duplicate hints from 2 servers and cache them all"
                 );
                 assert_eq!(
                     vec![
@@ -3869,7 +3977,7 @@ let c = 3;"#
                         ": i33".to_string(),
                     ],
                     visible_hint_labels(editor, cx),
-                    "lib.rs is added before main.rs , so its excerpts should be visible first"
+                    "lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
                 );
             })
             .unwrap();
@@ -3918,10 +4026,14 @@ let c = 3;"#
             .update(cx, |editor, _window, cx| {
                 assert_eq!(
                     vec![
+                        ": i32".to_string(),
                         ": i32".to_string(),
                         ": i33".to_string(),
+                        ": i33".to_string(),
+                        ": i34".to_string(),
                         ": i34".to_string(),
                         ": i35".to_string(),
+                        ": i35".to_string(),
                     ],
                     sorted_cached_hint_labels(editor, cx),
                     "No hint changes/duplicates should occur in the cache",