More inlay hint cache tests

Kirill Bulatov created

Change summary

crates/editor/src/inlay_hint_cache.rs | 991 ++++++++++++++++++++++++++--
1 file changed, 891 insertions(+), 100 deletions(-)

Detailed changes

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -70,20 +70,20 @@ impl ExcerptQuery {
         if self
             .dimensions
             .excerpt_range_start
-            .cmp(&self.dimensions.excerpt_visible_range_start, buffer)
+            .cmp(&visible_range.start, buffer)
             .is_lt()
         {
-            let mut end = self.dimensions.excerpt_visible_range_start;
+            let mut end = visible_range.start;
             end.offset -= 1;
             other_ranges.push(self.dimensions.excerpt_range_start..end);
         }
         if self
             .dimensions
             .excerpt_range_end
-            .cmp(&self.dimensions.excerpt_visible_range_end, buffer)
+            .cmp(&visible_range.end, buffer)
             .is_gt()
         {
-            let mut start = self.dimensions.excerpt_visible_range_end;
+            let mut start = visible_range.end;
             start.offset += 1;
             other_ranges.push(start..self.dimensions.excerpt_range_end);
         }
@@ -794,15 +794,19 @@ fn contains_position(
 mod tests {
     use std::sync::atomic::{AtomicU32, Ordering};
 
-    use crate::{serde_json::json, InlayHintSettings};
+    use crate::{
+        scroll::scroll_amount::ScrollAmount, serde_json::json, ExcerptRange, InlayHintSettings,
+    };
     use futures::StreamExt;
     use gpui::{TestAppContext, ViewHandle};
     use language::{
         language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
     };
     use lsp::FakeLanguageServer;
+    use parking_lot::Mutex;
     use project::{FakeFs, Project};
     use settings::SettingsStore;
+    use text::Point;
     use workspace::Workspace;
 
     use crate::editor_tests::update_test_settings;
@@ -820,6 +824,8 @@ mod tests {
                 show_other_hints: allowed_hint_kinds.contains(&None),
             })
         });
+
+        cx.foreground().start_waiting();
         let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
         let lsp_request_count = Arc::new(AtomicU32::new(0));
         fake_server
@@ -860,6 +866,7 @@ mod tests {
             .await;
         cx.foreground().finish_waiting();
         cx.foreground().run_until_parked();
+
         let mut edits_made = 1;
         editor.update(cx, |editor, cx| {
             let expected_layers = vec!["0".to_string()];
@@ -931,61 +938,6 @@ mod tests {
         });
     }
 
-    async fn prepare_test_objects(
-        cx: &mut TestAppContext,
-    ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
-        let mut language = Language::new(
-            LanguageConfig {
-                name: "Rust".into(),
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            Some(tree_sitter_rust::language()),
-        );
-        let mut fake_servers = language
-            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
-                capabilities: lsp::ServerCapabilities {
-                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-                    ..Default::default()
-                },
-                ..Default::default()
-            }))
-            .await;
-
-        let fs = FakeFs::new(cx.background());
-        fs.insert_tree(
-            "/a",
-            json!({
-                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
-                "other.rs": "// Test file",
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs, ["/a".as_ref()], cx).await;
-        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
-        let worktree_id = workspace.update(cx, |workspace, cx| {
-            workspace.project().read_with(cx, |project, cx| {
-                project.worktrees(cx).next().unwrap().read(cx).id()
-            })
-        });
-
-        cx.foreground().start_waiting();
-        let editor = workspace
-            .update(cx, |workspace, cx| {
-                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
-            })
-            .await
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap();
-
-        let fake_server = fake_servers.next().await.unwrap();
-
-        ("/a/main.rs", editor, fake_server)
-    }
-
     #[gpui::test]
     async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
         let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@@ -997,6 +949,8 @@ mod tests {
                 show_other_hints: allowed_hint_kinds.contains(&None),
             })
         });
+
+        cx.foreground().start_waiting();
         let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
         let lsp_request_count = Arc::new(AtomicU32::new(0));
         let another_lsp_request_count = Arc::clone(&lsp_request_count);
@@ -1057,15 +1011,15 @@ mod tests {
             );
             assert_eq!(
                 vec![
-                    "type hint".to_string(),
+                    "other hint".to_string(),
                     "parameter hint".to_string(),
-                    "other hint".to_string()
+                    "type hint".to_string(),
                 ],
                 cached_hint_labels(editor),
                 "Should get its first hints when opening the editor"
             );
             assert_eq!(
-                vec!["type hint".to_string(), "other hint".to_string()],
+                vec!["other hint".to_string(), "type hint".to_string()],
                 visible_hint_labels(editor, cx)
             );
             let inlay_cache = editor.inlay_hint_cache();
@@ -1092,15 +1046,15 @@ mod tests {
             );
             assert_eq!(
                 vec![
-                    "type hint".to_string(),
+                    "other hint".to_string(),
                     "parameter hint".to_string(),
-                    "other hint".to_string()
+                    "type hint".to_string(),
                 ],
                 cached_hint_labels(editor),
                 "Cached hints should not change due to allowed hint kinds settings update"
             );
             assert_eq!(
-                vec!["type hint".to_string(), "other hint".to_string()],
+                vec!["other hint".to_string(), "type hint".to_string()],
                 visible_hint_labels(editor, cx)
             );
             let inlay_cache = editor.inlay_hint_cache();
@@ -1123,15 +1077,15 @@ mod tests {
             ),
             (
                 HashSet::from_iter([None, Some(InlayHintKind::Type)]),
-                vec!["type hint".to_string(), "other hint".to_string()],
+                vec!["other hint".to_string(), "type hint".to_string()],
             ),
             (
                 HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
-                vec!["parameter hint".to_string(), "other hint".to_string()],
+                vec!["other hint".to_string(), "parameter hint".to_string()],
             ),
             (
                 HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
-                vec!["type hint".to_string(), "parameter hint".to_string()],
+                vec!["parameter hint".to_string(), "type hint".to_string()],
             ),
             (
                 HashSet::from_iter([
@@ -1140,9 +1094,9 @@ mod tests {
                     Some(InlayHintKind::Parameter),
                 ]),
                 vec![
-                    "type hint".to_string(),
-                    "parameter hint".to_string(),
                     "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
                 ],
             ),
         ] {
@@ -1165,9 +1119,9 @@ mod tests {
                 );
                 assert_eq!(
                     vec![
-                        "type hint".to_string(),
-                        "parameter hint".to_string(),
                         "other hint".to_string(),
+                        "parameter hint".to_string(),
+                        "type hint".to_string(),
                     ],
                     cached_hint_labels(editor),
                     "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
@@ -1267,9 +1221,9 @@ mod tests {
             );
             assert_eq!(
                 vec![
-                    "type hint".to_string(),
-                    "parameter hint".to_string(),
                     "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
                 ],
                 cached_hint_labels(editor),
                 "Should get its cached hints fully repopulated after the hints got reenabled"
@@ -1303,9 +1257,9 @@ mod tests {
             );
             assert_eq!(
                 vec![
-                    "type hint".to_string(),
-                    "parameter hint".to_string(),
                     "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
                 ],
                 cached_hint_labels(editor),
             );
@@ -1319,41 +1273,878 @@ mod tests {
         });
     }
 
-    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
-        cx.foreground().forbid_parking();
+    #[gpui::test]
+    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
 
-        cx.update(|cx| {
-            cx.set_global(SettingsStore::test(cx));
-            theme::init((), cx);
-            client::init_settings(cx);
-            language::init(cx);
-            Project::init_settings(cx);
-            workspace::init_settings(cx);
-            crate::init(cx);
+        cx.foreground().start_waiting();
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let fake_server = Arc::new(fake_server);
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+
+        let mut expected_changes = Vec::new();
+        for change_after_opening in [
+            "initial change #1",
+            "initial change #2",
+            "initial change #3",
+        ] {
+            editor.update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input(change_after_opening, cx);
+            });
+            expected_changes.push(change_after_opening);
+        }
+
+        cx.foreground().finish_waiting();
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+            );
+            let expected_hints = vec!["2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 1,
+                "Only one update should be registered in the cache after all cancellations"
+            );
         });
 
-        update_test_settings(cx, f);
+        let mut edits = Vec::new();
+        for async_later_change in [
+            "another change #1",
+            "another change #2",
+            "another change #3",
+        ] {
+            expected_changes.push(async_later_change);
+            let task_editor = editor.clone();
+            let mut task_cx = cx.clone();
+            edits.push(cx.foreground().spawn(async move {
+                task_editor.update(&mut task_cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                    editor.handle_input(async_later_change, cx);
+                });
+            }));
+        }
+        let _ = futures::future::join_all(edits).await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                3,
+                "Should query new hints one more time, for the last edit only"
+            );
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Should update the cache version once more, for the new change"
+            );
+        });
     }
 
-    fn cached_hint_labels(editor: &Editor) -> Vec<String> {
-        let mut labels = Vec::new();
-        for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
-            let excerpt_hints = excerpt_hints.read();
-            for (_, inlay) in excerpt_hints.hints.iter() {
-                match &inlay.label {
-                    project::InlayHintLabel::String(s) => labels.push(s.to_string()),
-                    _ => unreachable!(),
+    #[gpui::test]
+    async fn test_hint_refresh_request_cancellation(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        cx.foreground().start_waiting();
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let fake_server = Arc::new(fake_server);
+        let mut initial_refresh_tasks = Vec::new();
+        let task_cx = cx.clone();
+        let add_refresh_task = |tasks: &mut Vec<Task<()>>| {
+            let task_fake_server = Arc::clone(&fake_server);
+            tasks.push(task_cx.foreground().spawn(async move {
+                task_fake_server
+                    .request::<lsp::request::InlayHintRefreshRequest>(())
+                    .await
+                    .expect("inlay refresh request failed");
+            }))
+        };
+
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
                 }
+            })
+            .next()
+            .await;
+
+        add_refresh_task(&mut initial_refresh_tasks);
+        add_refresh_task(&mut initial_refresh_tasks);
+        let _ = futures::future::join_all(initial_refresh_tasks).await;
+
+        cx.foreground().finish_waiting();
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                3,
+                "Should query new hints once for editor opening, 2 times for every request"
+            );
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last refresh landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 1,
+                "Only one update should be registered in the cache after all cancellations"
+            );
+        });
+
+        let mut expected_changes = Vec::new();
+        let mut edits_and_refreshes = Vec::new();
+        add_refresh_task(&mut edits_and_refreshes);
+        for async_later_change in ["change #1", "change #2", "change #3"] {
+            expected_changes.push(async_later_change);
+            let task_editor = editor.clone();
+            let mut task_cx = cx.clone();
+            add_refresh_task(&mut edits_and_refreshes);
+            edits_and_refreshes.push(cx.foreground().spawn(async move {
+                task_editor.update(&mut task_cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                    editor.handle_input(async_later_change, cx);
+                });
+            }));
+            add_refresh_task(&mut edits_and_refreshes);
+        }
+        add_refresh_task(&mut edits_and_refreshes);
+        let _ = futures::future::join_all(edits_and_refreshes).await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
             }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                5,
+                "Should query new hints twice more, for last edit & refresh request after it"
+            );
+            let expected_hints = vec!["5".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit and refresh request only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Should update the cache version once since refresh did not get new hint updates"
+            );
+        });
+
+        let mut edits_and_refreshes = Vec::new();
+        add_refresh_task(&mut edits_and_refreshes);
+        for async_later_change in ["last change #1", "last change #2", "last change #3"] {
+            expected_changes.push(async_later_change);
+            let task_editor = editor.clone();
+            let mut task_cx = cx.clone();
+            add_refresh_task(&mut edits_and_refreshes);
+            edits_and_refreshes.push(cx.foreground().spawn(async move {
+                task_editor.update(&mut task_cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                    editor.handle_input(async_later_change, cx);
+                });
+            }));
         }
-        labels
+        let _ = futures::future::join_all(edits_and_refreshes).await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                6,
+                "Should query new hints once more, for last edit. All refresh tasks were before this edit hence should be cancelled."
+            );
+            let expected_hints = vec!["6".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 3,
+                "Should update the cache version once due to the new change"
+            );
+        });
     }
 
-    fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
-        editor
+    #[gpui::test]
+    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        cx.foreground().start_waiting();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let fake_server = fake_servers.next().await.unwrap();
+        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+
+                    task_lsp_request_ranges.lock().push(params.range);
+                    let query_start = params.range.start;
+                    let query_end = params.range.end;
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(
+                            (query_end.line - query_start.line) / 2,
+                            (query_end.character - query_start.character) / 2,
+                        ),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().finish_waiting();
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|range| range.start);
+            assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+            assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
+            assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
+
+            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
+                "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+            let expected_layers = vec!["1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should have hints from both LSP requests made for a big file"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Both LSP queries should've bumped the cache version"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
+            editor.handle_input("++++more text++++", cx);
+        });
+
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|range| range.start);
+            assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
+            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+            assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
+            assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
+            assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
+            assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
+            assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
+
+            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
+                "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
+            let expected_layers = vec!["4".to_string(), "5".to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "Should have hints from the new LSP response after edit");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 4, "Should update the cache for every LSP response with hints added");
+        });
+    }
+
+    #[gpui::test]
+    async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let language = Arc::new(language);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::clone(&language))
+        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "main.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "other.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(2, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(11, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 0)..Point::new(33, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 0)..Point::new(55, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 0)..Point::new(66, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 0)..Point::new(77, 0),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 1)..Point::new(2, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 1)..Point::new(11, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 1)..Point::new(33, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 1)..Point::new(55, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 1)..Point::new(66, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 1)..Point::new(77, 1),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        cx.foreground().start_waiting();
+        let (_, editor) =
+            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+
+        let fake_server = fake_servers.next().await.unwrap();
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| async move {
+                let hint_text = if params.text_document.uri
+                    == lsp::Url::from_file_path("/a/main.rs").unwrap()
+                {
+                    "main hint"
+                } else if params.text_document.uri
+                    == lsp::Url::from_file_path("/a/other.rs").unwrap()
+                {
+                    "other hint"
+                } else {
+                    panic!("unexpected uri: {:?}", params.text_document.uri);
+                };
+
+                let positions = [
+                    lsp::Position::new(0, 2),
+                    lsp::Position::new(4, 2),
+                    lsp::Position::new(22, 2),
+                    lsp::Position::new(44, 2),
+                    lsp::Position::new(56, 2),
+                    lsp::Position::new(67, 2),
+                ];
+                let out_of_range_hint = lsp::InlayHint {
+                    position: lsp::Position::new(
+                        params.range.start.line + 99,
+                        params.range.start.character + 99,
+                    ),
+                    label: lsp::InlayHintLabel::String(
+                        "out of excerpt range, should be ignored".to_string(),
+                    ),
+                    kind: None,
+                    text_edits: None,
+                    tooltip: None,
+                    padding_left: None,
+                    padding_right: None,
+                    data: None,
+                };
+                Ok(Some(
+                    std::iter::once(out_of_range_hint)
+                        .chain(positions.into_iter().enumerate().map(|(i, position)| {
+                            lsp::InlayHint {
+                                position,
+                                label: lsp::InlayHintLabel::String(format!("{hint_text} #{i}")),
+                                kind: None,
+                                text_edits: None,
+                                tooltip: None,
+                                padding_left: None,
+                                padding_right: None,
+                                data: None,
+                            }
+                        }))
+                        .collect(),
+                ))
+            })
+            .next()
+            .await;
+
+        cx.foreground().finish_waiting();
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+            ];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(0.9), cx);
+            editor.scroll_screen(&ScrollAmount::Page(0.9), cx);
+            editor.scroll_screen(&ScrollAmount::Page(0.9), cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 11);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(0.9), cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, further scrolls down should not bring more hints");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 11);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(-0.9), cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 11, "No updates should happen during scrolling already scolled buffer");
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
+            editor.handle_input("++++more text++++", cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was edited, hints for the edited buffer (1st) should be requeried for all of its excerpts, \
+unedited (2nd) buffer should have the same hint");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 12);
+        });
+    }
+
+    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        cx.foreground().forbid_parking();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            client::init_settings(cx);
+            language::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
+            crate::init(cx);
+        });
+
+        update_test_settings(cx, f);
+    }
+
+    async fn prepare_test_objects(
+        cx: &mut TestAppContext,
+    ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        let fake_server = fake_servers.next().await.unwrap();
+
+        ("/a/main.rs", editor, fake_server)
+    }
+
+    fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+        let mut labels = Vec::new();
+        for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+            let excerpt_hints = excerpt_hints.read();
+            for (_, inlay) in excerpt_hints.hints.iter() {
+                match &inlay.label {
+                    project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+                    _ => unreachable!(),
+                }
+            }
+        }
+
+        labels.sort();
+        labels
+    }
+
+    fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+        let mut zz = editor
             .visible_inlay_hints(cx)
             .into_iter()
             .map(|hint| hint.text.to_string())
-            .collect()
+            .collect::<Vec<_>>();
+        zz.sort();
+        zz
     }
 }