Rework inlay hints system (#40183)

Kirill Bulatov , Lukas Wirth , dino , and Lukas Wirth created

Closes https://github.com/zed-industries/zed/issues/40047
Closes https://github.com/zed-industries/zed/issues/24798
Closes https://github.com/zed-industries/zed/issues/24788

Before, each editor, even if it's the same buffer split in 2, was
querying for inlay hints separately, and storing the whole inlay hint
twice, in `Editor`'s `display_map` and its `inlay_hint_cache` fields.

Now, instead of `inlay_hint_cache`, each editor maintains a minimal set
of metadata (which area was queried by what task) instead, and all LSP
inlay hint data had been moved into `LspStore`, both local and remote
flavors store the data.
This allows Zed, as long as a buffer is open, to reuse the inlay hint
data similar to how document colors and code lens are now stored and
reused.

Unlike other reused LSP data, inlay hints data is the first one that's
possible to query by document ranges and previous version had issue with
caching and invalidating such ranges already queried for.
The new version re-approaches this by chunking the file into row ranges,
which are queried based on the editors' visible area.

Among the corresponding refactoring, one notable difference in inlays
display are multi buffers: buffers in them are not
[registered](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen)
in the language server until a caret/selection is placed inside their
excerpts inside the multi buffer.

New inlays code does not query language servers for unregistered
buffers, as servers usually respond with empty responses or errors in
such cases.

Release Notes:

- Reworked inlay hints to be less error-prone

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: dino <dinojoaocosta@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>

Change summary

Cargo.lock                                        |    1 
crates/agent_ui/src/acp/message_editor.rs         |   11 
crates/collab/src/rpc.rs                          |    1 
crates/collab/src/tests/editor_tests.rs           |  167 
crates/diagnostics/src/diagnostics_tests.rs       |    4 
crates/editor/src/display_map.rs                  |   27 
crates/editor/src/display_map/fold_map.rs         |    3 
crates/editor/src/display_map/inlay_map.rs        |  111 
crates/editor/src/editor.rs                       |  503 ---
crates/editor/src/editor_tests.rs                 |    4 
crates/editor/src/hover_links.rs                  |  194 -
crates/editor/src/hover_popover.rs                |   17 
crates/editor/src/inlays.rs                       |  193 +
crates/editor/src/inlays/inlay_hints.rs           | 1896 ++++++----------
crates/editor/src/lsp_colors.rs                   |   11 
crates/editor/src/movement.rs                     |    2 
crates/editor/src/proposed_changes_editor.rs      |   47 
crates/editor/src/test/editor_lsp_test_context.rs |   50 
crates/language/src/language.rs                   |   59 
crates/project/src/lsp_command.rs                 |    2 
crates/project/src/lsp_store.rs                   |  699 ++++-
crates/project/src/lsp_store/inlay_hint_cache.rs  |  221 +
crates/project/src/project.rs                     |   58 
crates/project/src/project_tests.rs               |   10 
crates/proto/proto/lsp.proto                      |    4 
crates/proto/src/proto.rs                         |    2 
crates/rpc/src/proto_client.rs                    |    5 
crates/search/Cargo.toml                          |    2 
crates/search/src/project_search.rs               |   98 
crates/util/src/paths.rs                          |    2 
crates/vim/src/motion.rs                          |    2 
31 files changed, 2,205 insertions(+), 2,201 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14955,6 +14955,7 @@ dependencies = [
  "futures 0.3.31",
  "gpui",
  "language",
+ "lsp",
  "menu",
  "project",
  "schemars 1.0.4",

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
 use collections::{HashMap, HashSet};
 use editor::{
     Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
-    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
+    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
     MultiBuffer, ToOffset,
     actions::Paste,
-    display_map::{Crease, CreaseId, FoldId, Inlay},
+    display_map::{Crease, CreaseId, FoldId},
 };
 use futures::{
     FutureExt as _,
@@ -29,7 +29,8 @@ use language::{Buffer, Language, language_settings::InlayHintKind};
 use language_model::LanguageModelImage;
 use postage::stream::Stream as _;
 use project::{
-    CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
+    CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath,
+    Worktree,
 };
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
@@ -75,7 +76,7 @@ pub enum MessageEditorEvent {
 
 impl EventEmitter<MessageEditorEvent> for MessageEditor {}
 
-const COMMAND_HINT_INLAY_ID: u32 = 0;
+const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
 
 impl MessageEditor {
     pub fn new(
@@ -151,7 +152,7 @@ impl MessageEditor {
                         let has_new_hint = !new_hints.is_empty();
                         editor.splice_inlays(
                             if has_hint {
-                                &[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
+                                &[COMMAND_HINT_INLAY_ID]
                             } else {
                                 &[]
                             },

crates/collab/src/rpc.rs 🔗

@@ -343,7 +343,6 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
             .add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
-            .add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
             .add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
             .add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)

crates/collab/src/tests/editor_tests.rs 🔗

@@ -1849,10 +1849,40 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         ..lsp::ServerCapabilities::default()
     };
     client_a.language_registry().add(rust_lang());
+
+    // Set up the language server to return an additional inlay hint on each request.
+    let edits_made = Arc::new(AtomicUsize::new(0));
+    let closure_edits_made = Arc::clone(&edits_made);
     let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
         "Rust",
         FakeLspAdapter {
             capabilities: capabilities.clone(),
+            initializer: Some(Box::new(move |fake_language_server| {
+                let closure_edits_made = closure_edits_made.clone();
+                fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
+                    move |params, _| {
+                        let edits_made_2 = Arc::clone(&closure_edits_made);
+                        async move {
+                            assert_eq!(
+                                params.text_document.uri,
+                                lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
+                            );
+                            let edits_made =
+                                AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
+                            Ok(Some(vec![lsp::InlayHint {
+                                position: lsp::Position::new(0, edits_made as u32),
+                                label: lsp::InlayHintLabel::String(edits_made.to_string()),
+                                kind: None,
+                                text_edits: None,
+                                tooltip: None,
+                                padding_left: None,
+                                padding_right: None,
+                                data: None,
+                            }]))
+                        }
+                    },
+                );
+            })),
             ..FakeLspAdapter::default()
         },
     );
@@ -1894,61 +1924,20 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .unwrap();
 
     let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
-    executor.start_waiting();
 
     // The host opens a rust file.
-    let _buffer_a = project_a
-        .update(cx_a, |project, cx| {
-            project.open_local_buffer(path!("/a/main.rs"), cx)
-        })
-        .await
-        .unwrap();
-    let editor_a = workspace_a
-        .update_in(cx_a, |workspace, window, cx| {
-            workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
+    let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
+        workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
+    });
     let fake_language_server = fake_language_servers.next().await.unwrap();
-
-    // Set up the language server to return an additional inlay hint on each request.
-    let edits_made = Arc::new(AtomicUsize::new(0));
-    let closure_edits_made = Arc::clone(&edits_made);
-    fake_language_server
-        .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-            let edits_made_2 = Arc::clone(&closure_edits_made);
-            async move {
-                assert_eq!(
-                    params.text_document.uri,
-                    lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
-                );
-                let edits_made = AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
-                Ok(Some(vec![lsp::InlayHint {
-                    position: lsp::Position::new(0, edits_made as u32),
-                    label: lsp::InlayHintLabel::String(edits_made.to_string()),
-                    kind: None,
-                    text_edits: None,
-                    tooltip: None,
-                    padding_left: None,
-                    padding_right: None,
-                    data: None,
-                }]))
-            }
-        })
-        .next()
-        .await
-        .unwrap();
-
+    let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
     executor.run_until_parked();
 
     let initial_edit = edits_made.load(atomic::Ordering::Acquire);
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![initial_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Host should get its first hints when opens an editor"
         );
     });
@@ -1963,10 +1952,10 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .unwrap();
 
     executor.run_until_parked();
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![initial_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Client should get its first hints when opens an editor"
         );
     });
@@ -1981,16 +1970,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     cx_b.focus(&editor_b);
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![after_client_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![after_client_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
 
@@ -2004,16 +1993,16 @@ async fn test_mutual_editor_inlay_hint_cache_update(
     cx_a.focus(&editor_a);
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![after_host_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![after_host_edit.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
         );
     });
 
@@ -2025,26 +2014,22 @@ async fn test_mutual_editor_inlay_hint_cache_update(
         .expect("inlay refresh request failed");
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert_eq!(
             vec![after_special_edit_for_refresh.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Host should react to /refresh LSP request"
         );
     });
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec![after_special_edit_for_refresh.to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Guest should get a /refresh LSP request propagated by host"
         );
     });
 }
 
-// This test started hanging on seed 2 after the theme settings
-// PR. The hypothesis is that it's been buggy for a while, but got lucky
-// on seeds.
-#[ignore]
 #[gpui::test(iterations = 10)]
 async fn test_inlay_hint_refresh_is_forwarded(
     cx_a: &mut TestAppContext,
@@ -2206,18 +2191,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
     executor.finish_waiting();
 
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert!(
-            extract_hint_labels(editor).is_empty(),
+            extract_hint_labels(editor, cx).is_empty(),
             "Host should get no hints due to them turned off"
         );
     });
 
     executor.run_until_parked();
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec!["initial hint".to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Client should get its first hints when opens an editor"
         );
     });
@@ -2229,18 +2214,18 @@ async fn test_inlay_hint_refresh_is_forwarded(
         .into_response()
         .expect("inlay refresh request failed");
     executor.run_until_parked();
-    editor_a.update(cx_a, |editor, _| {
+    editor_a.update(cx_a, |editor, cx| {
         assert!(
-            extract_hint_labels(editor).is_empty(),
+            extract_hint_labels(editor, cx).is_empty(),
             "Host should get no hints due to them turned off, even after the /refresh"
         );
     });
 
     executor.run_until_parked();
-    editor_b.update(cx_b, |editor, _| {
+    editor_b.update(cx_b, |editor, cx| {
         assert_eq!(
             vec!["other hint".to_string()],
-            extract_hint_labels(editor),
+            extract_hint_labels(editor, cx),
             "Guest should get a /refresh LSP request propagated by host despite host hints are off"
         );
     });
@@ -4217,15 +4202,35 @@ fn tab_undo_assert(
     cx_b.assert_editor_state(expected_initial);
 }
 
-fn extract_hint_labels(editor: &Editor) -> Vec<String> {
-    let mut labels = Vec::new();
-    for hint in editor.inlay_hint_cache().hints() {
-        match hint.label {
-            project::InlayHintLabel::String(s) => labels.push(s),
-            _ => unreachable!(),
-        }
+fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
+    let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+
+    let mut all_cached_labels = Vec::new();
+    let mut all_fetched_hints = Vec::new();
+    for buffer in editor.buffer().read(cx).all_buffers() {
+        lsp_store.update(cx, |lsp_store, cx| {
+            let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
+            all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
+                let mut label = hint.text().to_string();
+                if hint.padding_left {
+                    label.insert(0, ' ');
+                }
+                if hint.padding_right {
+                    label.push_str(" ");
+                }
+                label
+            }));
+            all_fetched_hints.extend(hints.all_fetched_hints());
+        });
     }
-    labels
+
+    assert!(
+        all_fetched_hints.is_empty(),
+        "Did not expect background hints fetch tasks, but got {} of them",
+        all_fetched_hints.len()
+    );
+
+    all_cached_labels
 }
 
 #[track_caller]

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -1,9 +1,9 @@
 use super::*;
 use collections::{HashMap, HashSet};
 use editor::{
-    DisplayPoint, EditorSettings,
+    DisplayPoint, EditorSettings, Inlay,
     actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
-    display_map::{DisplayRow, Inlay},
+    display_map::DisplayRow,
     test::{
         editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext,
         editor_test_context::EditorTestContext,

crates/editor/src/display_map.rs 🔗

@@ -27,7 +27,7 @@ mod tab_map;
 mod wrap_map;
 
 use crate::{
-    EditorStyle, InlayId, RowExt, hover_links::InlayHighlight, movement::TextLayoutDetails,
+    EditorStyle, RowExt, hover_links::InlayHighlight, inlays::Inlay, movement::TextLayoutDetails,
 };
 pub use block_map::{
     Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
@@ -42,7 +42,6 @@ pub use fold_map::{
     ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
 };
 use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
-pub use inlay_map::Inlay;
 use inlay_map::InlaySnapshot;
 pub use inlay_map::{InlayOffset, InlayPoint};
 pub use invisibles::{is_invisible, replacement};
@@ -50,9 +49,10 @@ use language::{
     OffsetUtf16, Point, Subscription as BufferSubscription, language_settings::language_settings,
 };
 use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
-    MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
+    Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
+    RowInfo, ToOffset, ToPoint,
 };
+use project::InlayId;
 use project::project_settings::DiagnosticSeverity;
 use serde::Deserialize;
 
@@ -594,25 +594,6 @@ impl DisplayMap {
         self.block_map.read(snapshot, edits);
     }
 
-    pub fn remove_inlays_for_excerpts(
-        &mut self,
-        excerpts_removed: &[ExcerptId],
-        cx: &mut Context<Self>,
-    ) {
-        let to_remove = self
-            .inlay_map
-            .current_inlays()
-            .filter_map(|inlay| {
-                if excerpts_removed.contains(&inlay.position.excerpt_id) {
-                    Some(inlay.id)
-                } else {
-                    None
-                }
-            })
-            .collect::<Vec<_>>();
-        self.splice_inlays(&to_remove, Vec::new(), cx);
-    }
-
     fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
         let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
         let language = buffer

crates/editor/src/display_map/fold_map.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{InlayId, display_map::inlay_map::InlayChunk};
+use crate::display_map::inlay_map::InlayChunk;
 
 use super::{
     Highlights,
@@ -9,6 +9,7 @@ use language::{Edit, HighlightId, Point, TextSummary};
 use multi_buffer::{
     Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset,
 };
+use project::InlayId;
 use std::{
     any::TypeId,
     cmp::{self, Ordering},

crates/editor/src/display_map/inlay_map.rs 🔗

@@ -1,17 +1,18 @@
-use crate::{ChunkRenderer, HighlightStyles, InlayId};
+use crate::{
+    ChunkRenderer, HighlightStyles,
+    inlays::{Inlay, InlayContent},
+};
 use collections::BTreeSet;
-use gpui::{Hsla, Rgba};
 use language::{Chunk, Edit, Point, TextSummary};
-use multi_buffer::{
-    Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset,
-};
+use multi_buffer::{MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset};
+use project::InlayId;
 use std::{
     cmp,
     ops::{Add, AddAssign, Range, Sub, SubAssign},
-    sync::{Arc, OnceLock},
+    sync::Arc,
 };
 use sum_tree::{Bias, Cursor, Dimensions, SumTree};
-use text::{ChunkBitmaps, Patch, Rope};
+use text::{ChunkBitmaps, Patch};
 use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
 
 use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
@@ -37,85 +38,6 @@ enum Transform {
     Inlay(Inlay),
 }
 
-#[derive(Debug, Clone)]
-pub struct Inlay {
-    pub id: InlayId,
-    pub position: Anchor,
-    pub content: InlayContent,
-}
-
-#[derive(Debug, Clone)]
-pub enum InlayContent {
-    Text(text::Rope),
-    Color(Hsla),
-}
-
-impl Inlay {
-    pub fn hint(id: u32, position: Anchor, hint: &project::InlayHint) -> Self {
-        let mut text = hint.text();
-        if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
-            text.push(" ");
-        }
-        if hint.padding_left && text.chars_at(0).next() != Some(' ') {
-            text.push_front(" ");
-        }
-        Self {
-            id: InlayId::Hint(id),
-            position,
-            content: InlayContent::Text(text),
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn mock_hint(id: u32, position: Anchor, text: impl Into<Rope>) -> Self {
-        Self {
-            id: InlayId::Hint(id),
-            position,
-            content: InlayContent::Text(text.into()),
-        }
-    }
-
-    pub fn color(id: u32, position: Anchor, color: Rgba) -> Self {
-        Self {
-            id: InlayId::Color(id),
-            position,
-            content: InlayContent::Color(color.into()),
-        }
-    }
-
-    pub fn edit_prediction<T: Into<Rope>>(id: u32, position: Anchor, text: T) -> Self {
-        Self {
-            id: InlayId::EditPrediction(id),
-            position,
-            content: InlayContent::Text(text.into()),
-        }
-    }
-
-    pub fn debugger<T: Into<Rope>>(id: u32, position: Anchor, text: T) -> Self {
-        Self {
-            id: InlayId::DebuggerValue(id),
-            position,
-            content: InlayContent::Text(text.into()),
-        }
-    }
-
-    pub fn text(&self) -> &Rope {
-        static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
-        match &self.content {
-            InlayContent::Text(text) => text,
-            InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")),
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn get_color(&self) -> Option<Hsla> {
-        match self.content {
-            InlayContent::Color(color) => Some(color),
-            _ => None,
-        }
-    }
-}
-
 impl sum_tree::Item for Transform {
     type Summary = TransformSummary;
 
@@ -750,7 +672,7 @@ impl InlayMap {
     #[cfg(test)]
     pub(crate) fn randomly_mutate(
         &mut self,
-        next_inlay_id: &mut u32,
+        next_inlay_id: &mut usize,
         rng: &mut rand::rngs::StdRng,
     ) -> (InlaySnapshot, Vec<InlayEdit>) {
         use rand::prelude::*;
@@ -1245,17 +1167,18 @@ const fn is_utf8_char_boundary(byte: u8) -> bool {
 mod tests {
     use super::*;
     use crate::{
-        InlayId, MultiBuffer,
+        MultiBuffer,
         display_map::{HighlightKey, InlayHighlights, TextHighlights},
         hover_links::InlayHighlight,
     };
     use gpui::{App, HighlightStyle};
+    use multi_buffer::Anchor;
     use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
     use sum_tree::TreeMap;
-    use text::Patch;
+    use text::{Patch, Rope};
     use util::RandomCharIter;
     use util::post_inc;
 
@@ -1263,7 +1186,7 @@ mod tests {
     fn test_inlay_properties_label_padding() {
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("a".to_string()),
@@ -1283,7 +1206,7 @@ mod tests {
 
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("a".to_string()),
@@ -1303,7 +1226,7 @@ mod tests {
 
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String(" a ".to_string()),
@@ -1323,7 +1246,7 @@ mod tests {
 
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String(" a ".to_string()),
@@ -1346,7 +1269,7 @@ mod tests {
     fn test_inlay_hint_padding_with_multibyte_chars() {
         assert_eq!(
             Inlay::hint(
-                0,
+                InlayId::Hint(0),
                 Anchor::min(),
                 &InlayHint {
                     label: InlayHintLabel::String("🎨".to_string()),

crates/editor/src/editor.rs 🔗

@@ -7,7 +7,6 @@
 //! * [`element`] — the place where all rendering happens
 //! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them.
 //!   Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.).
-//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly.
 //!
 //! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s).
 //!
@@ -24,7 +23,7 @@ mod highlight_matching_bracket;
 mod hover_links;
 pub mod hover_popover;
 mod indent_guides;
-mod inlay_hint_cache;
+mod inlays;
 pub mod items;
 mod jsx_tag_auto_close;
 mod linked_editing_ranges;
@@ -61,6 +60,7 @@ pub use element::{
 };
 pub use git::blame::BlameRenderer;
 pub use hover_popover::hover_markdown_style;
+pub use inlays::Inlay;
 pub use items::MAX_TAB_TITLE_LEN;
 pub use lsp::CompletionContext;
 pub use lsp_ext::lsp_tasks;
@@ -112,10 +112,10 @@ use gpui::{
     div, point, prelude::*, pulsating_between, px, relative, size,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
-use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
+use hover_links::{HoverLink, HoveredLinkState, find_file};
 use hover_popover::{HoverState, hide_hover};
 use indent_guides::ActiveIndentGuidesState;
-use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
+use inlays::{InlaySplice, inlay_hints::InlayHintRefreshReason};
 use itertools::{Either, Itertools};
 use language::{
     AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
@@ -124,8 +124,8 @@ use language::{
     IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal,
     TextObject, TransactionId, TreeSitterOptions, WordsQuery,
     language_settings::{
-        self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
-        all_language_settings, language_settings,
+        self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
+        language_settings,
     },
     point_from_lsp, point_to_lsp, text_diff_with_options,
 };
@@ -146,9 +146,9 @@ use parking_lot::Mutex;
 use persistence::DB;
 use project::{
     BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
-    CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint,
-    Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath,
-    ProjectTransaction, TaskSourceKind,
+    CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
+    InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
+    ProjectPath, ProjectTransaction, TaskSourceKind,
     debugger::{
         breakpoint_store::{
             Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -157,7 +157,10 @@ use project::{
         session::{Session, SessionEvent},
     },
     git_store::{GitStoreEvent, RepositoryEvent},
-    lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
+    lsp_store::{
+        CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget,
+        OpenLspBufferHandle,
+    },
     project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
 };
 use rand::seq::SliceRandom;
@@ -178,7 +181,7 @@ use std::{
     iter::{self, Peekable},
     mem,
     num::NonZeroU32,
-    ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
+    ops::{Deref, DerefMut, Not, Range, RangeInclusive},
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
@@ -208,6 +211,10 @@ use crate::{
     code_context_menus::CompletionsMenuSource,
     editor_settings::MultiCursorModifier,
     hover_links::{find_url, find_url_from_range},
+    inlays::{
+        InlineValueCache,
+        inlay_hints::{LspInlayHintData, inlay_hint_settings},
+    },
     scroll::{ScrollOffset, ScrollPixelOffset},
     signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
 };
@@ -261,42 +268,6 @@ impl ReportEditorEvent {
     }
 }
 
-struct InlineValueCache {
-    enabled: bool,
-    inlays: Vec<InlayId>,
-    refresh_task: Task<Option<()>>,
-}
-
-impl InlineValueCache {
-    fn new(enabled: bool) -> Self {
-        Self {
-            enabled,
-            inlays: Vec::new(),
-            refresh_task: Task::ready(None),
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum InlayId {
-    EditPrediction(u32),
-    DebuggerValue(u32),
-    // LSP
-    Hint(u32),
-    Color(u32),
-}
-
-impl InlayId {
-    fn id(&self) -> u32 {
-        match self {
-            Self::EditPrediction(id) => *id,
-            Self::DebuggerValue(id) => *id,
-            Self::Hint(id) => *id,
-            Self::Color(id) => *id,
-        }
-    }
-}
-
 pub enum ActiveDebugLine {}
 pub enum DebugStackFrameLine {}
 enum DocumentHighlightRead {}
@@ -1124,9 +1095,8 @@ pub struct Editor {
     edit_prediction_preview: EditPredictionPreview,
     edit_prediction_indent_conflict: bool,
     edit_prediction_requires_modifier_in_indent_conflict: bool,
-    inlay_hint_cache: InlayHintCache,
-    next_inlay_id: u32,
-    next_color_inlay_id: u32,
+    next_inlay_id: usize,
+    next_color_inlay_id: usize,
     _subscriptions: Vec<Subscription>,
     pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
     gutter_dimensions: GutterDimensions,
@@ -1193,10 +1163,19 @@ pub struct Editor {
     colors: Option<LspColorData>,
     post_scroll_update: Task<()>,
     refresh_colors_task: Task<()>,
+    inlay_hints: Option<LspInlayHintData>,
     folding_newlines: Task<()>,
     pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
 }
 
+fn debounce_value(debounce_ms: u64) -> Option<Duration> {
+    if debounce_ms > 0 {
+        Some(Duration::from_millis(debounce_ms))
+    } else {
+        None
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
 enum NextScrollCursorCenterTopBottom {
     #[default]
@@ -1621,31 +1600,6 @@ pub enum GotoDefinitionKind {
     Implementation,
 }
 
-#[derive(Debug, Clone)]
-enum InlayHintRefreshReason {
-    ModifiersChanged(bool),
-    Toggle(bool),
-    SettingsChange(InlayHintSettings),
-    NewLinesShown,
-    BufferEdited(HashSet<Arc<Language>>),
-    RefreshRequested,
-    ExcerptsRemoved(Vec<ExcerptId>),
-}
-
-impl InlayHintRefreshReason {
-    fn description(&self) -> &'static str {
-        match self {
-            Self::ModifiersChanged(_) => "modifiers changed",
-            Self::Toggle(_) => "toggle",
-            Self::SettingsChange(_) => "settings change",
-            Self::NewLinesShown => "new lines shown",
-            Self::BufferEdited(_) => "buffer edited",
-            Self::RefreshRequested => "refresh requested",
-            Self::ExcerptsRemoved(_) => "excerpts removed",
-        }
-    }
-}
-
 pub enum FormatTarget {
     Buffers(HashSet<Entity<Buffer>>),
     Ranges(Vec<Range<MultiBufferPoint>>),
@@ -1881,8 +1835,11 @@ impl Editor {
                     project::Event::RefreshCodeLens => {
                         // we always query lens with actions, without storing them, always refreshing them
                     }
-                    project::Event::RefreshInlayHints => {
-                        editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
+                    project::Event::RefreshInlayHints(server_id) => {
+                        editor.refresh_inlay_hints(
+                            InlayHintRefreshReason::RefreshRequested(*server_id),
+                            cx,
+                        );
                     }
                     project::Event::LanguageServerRemoved(..) => {
                         if editor.tasks_update_task.is_none() {
@@ -1919,17 +1876,12 @@ impl Editor {
                     project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
                         let buffer_id = *buffer_id;
                         if editor.buffer().read(cx).buffer(buffer_id).is_some() {
-                            let registered = editor.register_buffer(buffer_id, cx);
-                            if registered {
-                                editor.update_lsp_data(Some(buffer_id), window, cx);
-                                editor.refresh_inlay_hints(
-                                    InlayHintRefreshReason::RefreshRequested,
-                                    cx,
-                                );
-                                refresh_linked_ranges(editor, window, cx);
-                                editor.refresh_code_actions(window, cx);
-                                editor.refresh_document_highlights(cx);
-                            }
+                            editor.register_buffer(buffer_id, cx);
+                            editor.update_lsp_data(Some(buffer_id), window, cx);
+                            editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+                            refresh_linked_ranges(editor, window, cx);
+                            editor.refresh_code_actions(window, cx);
+                            editor.refresh_document_highlights(cx);
                         }
                     }
 
@@ -2200,7 +2152,6 @@ impl Editor {
             diagnostics_enabled: full_mode,
             word_completions_enabled: full_mode,
             inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
-            inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
             gutter_hovered: false,
             pixel_position_of_newest_cursor: None,
             last_bounds: None,
@@ -2266,6 +2217,7 @@ impl Editor {
             pull_diagnostics_task: Task::ready(()),
             colors: None,
             refresh_colors_task: Task::ready(()),
+            inlay_hints: None,
             next_color_inlay_id: 0,
             post_scroll_update: Task::ready(()),
             linked_edit_ranges: Default::default(),
@@ -2403,13 +2355,15 @@ impl Editor {
 
             editor.go_to_active_debug_line(window, cx);
 
-            if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
-                editor.register_buffer(buffer.read(cx).remote_id(), cx);
-            }
-
             editor.minimap =
                 editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
             editor.colors = Some(LspColorData::new(cx));
+            editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings));
+
+            if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
+                editor.register_buffer(buffer.read(cx).remote_id(), cx);
+            }
+            editor.update_lsp_data(None, window, cx);
             editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
         }
 
@@ -5198,179 +5152,8 @@ impl Editor {
         }
     }
 
-    pub fn toggle_inline_values(
-        &mut self,
-        _: &ToggleInlineValues,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
-
-        self.refresh_inline_values(cx);
-    }
-
-    pub fn toggle_inlay_hints(
-        &mut self,
-        _: &ToggleInlayHints,
-        _: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.refresh_inlay_hints(
-            InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
-            cx,
-        );
-    }
-
-    pub fn inlay_hints_enabled(&self) -> bool {
-        self.inlay_hint_cache.enabled
-    }
-
-    pub fn inline_values_enabled(&self) -> bool {
-        self.inline_value_cache.enabled
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn inline_value_inlays(&self, cx: &App) -> Vec<Inlay> {
-        self.display_map
-            .read(cx)
-            .current_inlays()
-            .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
-            .cloned()
-            .collect()
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn all_inlays(&self, cx: &App) -> Vec<Inlay> {
-        self.display_map
-            .read(cx)
-            .current_inlays()
-            .cloned()
-            .collect()
-    }
-
-    fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
-        if self.semantics_provider.is_none() || !self.mode.is_full() {
-            return;
-        }
-
-        let reason_description = reason.description();
-        let ignore_debounce = matches!(
-            reason,
-            InlayHintRefreshReason::SettingsChange(_)
-                | InlayHintRefreshReason::Toggle(_)
-                | InlayHintRefreshReason::ExcerptsRemoved(_)
-                | InlayHintRefreshReason::ModifiersChanged(_)
-        );
-        let (invalidate_cache, required_languages) = match reason {
-            InlayHintRefreshReason::ModifiersChanged(enabled) => {
-                match self.inlay_hint_cache.modifiers_override(enabled) {
-                    Some(enabled) => {
-                        if enabled {
-                            (InvalidationStrategy::RefreshRequested, None)
-                        } else {
-                            self.clear_inlay_hints(cx);
-                            return;
-                        }
-                    }
-                    None => return,
-                }
-            }
-            InlayHintRefreshReason::Toggle(enabled) => {
-                if self.inlay_hint_cache.toggle(enabled) {
-                    if enabled {
-                        (InvalidationStrategy::RefreshRequested, None)
-                    } else {
-                        self.clear_inlay_hints(cx);
-                        return;
-                    }
-                } else {
-                    return;
-                }
-            }
-            InlayHintRefreshReason::SettingsChange(new_settings) => {
-                match self.inlay_hint_cache.update_settings(
-                    &self.buffer,
-                    new_settings,
-                    self.visible_inlay_hints(cx).cloned().collect::<Vec<_>>(),
-                    cx,
-                ) {
-                    ControlFlow::Break(Some(InlaySplice {
-                        to_remove,
-                        to_insert,
-                    })) => {
-                        self.splice_inlays(&to_remove, to_insert, cx);
-                        return;
-                    }
-                    ControlFlow::Break(None) => return,
-                    ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
-                }
-            }
-            InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
-                if let Some(InlaySplice {
-                    to_remove,
-                    to_insert,
-                }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
-                {
-                    self.splice_inlays(&to_remove, to_insert, cx);
-                }
-                self.display_map.update(cx, |display_map, cx| {
-                    display_map.remove_inlays_for_excerpts(&excerpts_removed, cx)
-                });
-                return;
-            }
-            InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
-            InlayHintRefreshReason::BufferEdited(buffer_languages) => {
-                (InvalidationStrategy::BufferEdited, Some(buffer_languages))
-            }
-            InlayHintRefreshReason::RefreshRequested => {
-                (InvalidationStrategy::RefreshRequested, None)
-            }
-        };
-
-        let mut visible_excerpts = self.visible_excerpts(required_languages.as_ref(), cx);
-        visible_excerpts.retain(|_, (buffer, _, _)| {
-            self.registered_buffers
-                .contains_key(&buffer.read(cx).remote_id())
-        });
-
-        if let Some(InlaySplice {
-            to_remove,
-            to_insert,
-        }) = self.inlay_hint_cache.spawn_hint_refresh(
-            reason_description,
-            visible_excerpts,
-            invalidate_cache,
-            ignore_debounce,
-            cx,
-        ) {
-            self.splice_inlays(&to_remove, to_insert, cx);
-        }
-    }
-
-    pub fn clear_inlay_hints(&self, cx: &mut Context<Editor>) {
-        self.splice_inlays(
-            &self
-                .visible_inlay_hints(cx)
-                .map(|inlay| inlay.id)
-                .collect::<Vec<_>>(),
-            Vec::new(),
-            cx,
-        );
-    }
-
-    fn visible_inlay_hints<'a>(
-        &'a self,
-        cx: &'a Context<Editor>,
-    ) -> impl Iterator<Item = &'a Inlay> {
-        self.display_map
-            .read(cx)
-            .current_inlays()
-            .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
-    }
-
     pub fn visible_excerpts(
         &self,
-        restrict_to_languages: Option<&HashSet<Arc<Language>>>,
         cx: &mut Context<Editor>,
     ) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
         let Some(project) = self.project() else {
@@ -5389,9 +5172,8 @@ impl Editor {
                 + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
             Bias::Left,
         );
-        let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
         multi_buffer_snapshot
-            .range_to_buffer_ranges(multi_buffer_visible_range)
+            .range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end)
             .into_iter()
             .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
             .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
@@ -5401,23 +5183,17 @@ impl Editor {
                     .read(cx)
                     .entry_for_id(buffer_file.project_entry_id()?)?;
                 if worktree_entry.is_ignored {
-                    return None;
-                }
-
-                let language = buffer.language()?;
-                if let Some(restrict_to_languages) = restrict_to_languages
-                    && !restrict_to_languages.contains(language)
-                {
-                    return None;
+                    None
+                } else {
+                    Some((
+                        excerpt_id,
+                        (
+                            multi_buffer.buffer(buffer.remote_id()).unwrap(),
+                            buffer.version().clone(),
+                            excerpt_visible_range,
+                        ),
+                    ))
                 }
-                Some((
-                    excerpt_id,
-                    (
-                        multi_buffer.buffer(buffer.remote_id()).unwrap(),
-                        buffer.version().clone(),
-                        excerpt_visible_range,
-                    ),
-                ))
             })
             .collect()
     }
@@ -5433,18 +5209,6 @@ impl Editor {
         }
     }
 
-    pub fn splice_inlays(
-        &self,
-        to_remove: &[InlayId],
-        to_insert: Vec<Inlay>,
-        cx: &mut Context<Self>,
-    ) {
-        self.display_map.update(cx, |display_map, cx| {
-            display_map.splice_inlays(to_remove, to_insert, cx)
-        });
-        cx.notify();
-    }
-
     fn trigger_on_type_formatting(
         &self,
         input: String,
@@ -17618,9 +17382,9 @@ impl Editor {
                         HashSet::default(),
                         cx,
                     );
-                    cx.emit(project::Event::RefreshInlayHints);
                 });
             });
+            self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
         }
     }
 
@@ -20808,18 +20572,6 @@ impl Editor {
         cx.notify();
     }
 
-    pub(crate) fn highlight_inlays<T: 'static>(
-        &mut self,
-        highlights: Vec<InlayHighlight>,
-        style: HighlightStyle,
-        cx: &mut Context<Self>,
-    ) {
-        self.display_map.update(cx, |map, _| {
-            map.highlight_inlays(TypeId::of::<T>(), highlights, style)
-        });
-        cx.notify();
-    }
-
     pub fn text_highlights<'a, T: 'static>(
         &'a self,
         cx: &'a App,
@@ -20970,38 +20722,19 @@ impl Editor {
                     self.update_visible_edit_prediction(window, cx);
                 }
 
-                if let Some(edited_buffer) = edited_buffer {
-                    if edited_buffer.read(cx).file().is_none() {
+                if let Some(buffer) = edited_buffer {
+                    if buffer.read(cx).file().is_none() {
                         cx.emit(EditorEvent::TitleChanged);
                     }
 
-                    let buffer_id = edited_buffer.read(cx).remote_id();
-                    if let Some(project) = self.project.clone() {
+                    if self.project.is_some() {
+                        let buffer_id = buffer.read(cx).remote_id();
                         self.register_buffer(buffer_id, cx);
                         self.update_lsp_data(Some(buffer_id), window, cx);
-                        #[allow(clippy::mutable_key_type)]
-                        let languages_affected = multibuffer.update(cx, |multibuffer, cx| {
-                            multibuffer
-                                .all_buffers()
-                                .into_iter()
-                                .filter_map(|buffer| {
-                                    buffer.update(cx, |buffer, cx| {
-                                        let language = buffer.language()?;
-                                        let should_discard = project.update(cx, |project, cx| {
-                                            project.is_local()
-                                                && !project.has_language_servers_for(buffer, cx)
-                                        });
-                                        should_discard.not().then_some(language.clone())
-                                    })
-                                })
-                                .collect::<HashSet<_>>()
-                        });
-                        if !languages_affected.is_empty() {
-                            self.refresh_inlay_hints(
-                                InlayHintRefreshReason::BufferEdited(languages_affected),
-                                cx,
-                            );
-                        }
+                        self.refresh_inlay_hints(
+                            InlayHintRefreshReason::BufferEdited(buffer_id),
+                            cx,
+                        );
                     }
                 }
 
@@ -21048,6 +20781,9 @@ impl Editor {
                 ids,
                 removed_buffer_ids,
             } => {
+                if let Some(inlay_hints) = &mut self.inlay_hints {
+                    inlay_hints.remove_inlay_chunk_data(removed_buffer_ids);
+                }
                 self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
                 for buffer_id in removed_buffer_ids {
                     self.registered_buffers.remove(buffer_id);
@@ -21222,7 +20958,7 @@ impl Editor {
         if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
             colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
         }) {
-            if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() {
+            if !inlay_splice.is_empty() {
                 self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
             }
             self.refresh_colors_for_visible_range(None, window, cx);
@@ -21684,10 +21420,6 @@ impl Editor {
         mouse_context_menu::deploy_context_menu(self, None, position, window, cx);
     }
 
-    pub fn inlay_hint_cache(&self) -> &InlayHintCache {
-        &self.inlay_hint_cache
-    }
-
     pub fn replay_insert_event(
         &mut self,
         text: &str,
@@ -21726,21 +21458,6 @@ impl Editor {
         self.handle_input(text, window, cx);
     }
 
-    pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
-        let Some(provider) = self.semantics_provider.as_ref() else {
-            return false;
-        };
-
-        let mut supports = false;
-        self.buffer().update(cx, |this, cx| {
-            this.for_each_buffer(|buffer| {
-                supports |= provider.supports_inlay_hints(buffer, cx);
-            });
-        });
-
-        supports
-    }
-
     pub fn is_focused(&self, window: &Window) -> bool {
         self.focus_handle.is_focused(window)
     }
@@ -22156,12 +21873,12 @@ impl Editor {
         if self.ignore_lsp_data() {
             return;
         }
-        for (_, (visible_buffer, _, _)) in self.visible_excerpts(None, cx) {
+        for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) {
             self.register_buffer(visible_buffer.read(cx).remote_id(), cx);
         }
     }
 
-    fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) -> bool {
+    fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
         if !self.registered_buffers.contains_key(&buffer_id)
             && let Some(project) = self.project.as_ref()
         {
@@ -22172,13 +21889,10 @@ impl Editor {
                         project.register_buffer_with_language_servers(&buffer, cx),
                     );
                 });
-                return true;
             } else {
                 self.registered_buffers.remove(&buffer_id);
             }
         }
-
-        false
     }
 
     fn ignore_lsp_data(&self) -> bool {
@@ -22886,20 +22600,23 @@ pub trait SemanticsProvider {
         cx: &mut App,
     ) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
 
-    fn inlay_hints(
+    fn applicable_inlay_chunks(
         &self,
-        buffer_handle: Entity<Buffer>,
-        range: Range<text::Anchor>,
-        cx: &mut App,
-    ) -> Option<Task<anyhow::Result<Vec<InlayHint>>>>;
+        buffer_id: BufferId,
+        ranges: &[Range<text::Anchor>],
+        cx: &App,
+    ) -> Vec<Range<BufferRow>>;
+
+    fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App);
 
-    fn resolve_inlay_hint(
+    fn inlay_hints(
         &self,
-        hint: InlayHint,
-        buffer_handle: Entity<Buffer>,
-        server_id: LanguageServerId,
+        invalidate: InvalidationStrategy,
+        buffer: Entity<Buffer>,
+        ranges: Vec<Range<text::Anchor>>,
+        known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
         cx: &mut App,
-    ) -> Option<Task<anyhow::Result<InlayHint>>>;
+    ) -> Option<HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>>>;
 
     fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool;
 
@@ -23392,26 +23109,34 @@ impl SemanticsProvider for Entity<Project> {
         })
     }
 
-    fn inlay_hints(
+    fn applicable_inlay_chunks(
         &self,
-        buffer_handle: Entity<Buffer>,
-        range: Range<text::Anchor>,
-        cx: &mut App,
-    ) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
-        Some(self.update(cx, |project, cx| {
-            project.inlay_hints(buffer_handle, range, cx)
-        }))
+        buffer_id: BufferId,
+        ranges: &[Range<text::Anchor>],
+        cx: &App,
+    ) -> Vec<Range<BufferRow>> {
+        self.read(cx)
+            .lsp_store()
+            .read(cx)
+            .applicable_inlay_chunks(buffer_id, ranges)
+    }
+
+    fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
+        self.read(cx).lsp_store().update(cx, |lsp_store, _| {
+            lsp_store.invalidate_inlay_hints(for_buffers)
+        });
     }
 
-    fn resolve_inlay_hint(
+    fn inlay_hints(
         &self,
-        hint: InlayHint,
-        buffer_handle: Entity<Buffer>,
-        server_id: LanguageServerId,
+        invalidate: InvalidationStrategy,
+        buffer: Entity<Buffer>,
+        ranges: Vec<Range<text::Anchor>>,
+        known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
         cx: &mut App,
-    ) -> Option<Task<anyhow::Result<InlayHint>>> {
-        Some(self.update(cx, |project, cx| {
-            project.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
+    ) -> Option<HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>>> {
+        Some(self.read(cx).lsp_store().update(cx, |lsp_store, cx| {
+            lsp_store.inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
         }))
     }
 
@@ -23460,16 +23185,6 @@ impl SemanticsProvider for Entity<Project> {
     }
 }
 
-fn inlay_hint_settings(
-    location: Anchor,
-    snapshot: &MultiBufferSnapshot,
-    cx: &mut Context<Editor>,
-) -> InlayHintSettings {
-    let file = snapshot.file_at(location);
-    let language = snapshot.language_at(location).map(|l| l.name());
-    language_settings(language, file, cx).inlay_hints
-}
-
 fn consume_contiguous_rows(
     contiguous_row_selections: &mut Vec<Selection<Point>>,
     selection: &Selection<Point>,

crates/editor/src/editor_tests.rs 🔗

@@ -31,6 +31,7 @@ use language::{
     tree_sitter_python,
 };
 use language_settings::Formatter;
+use languages::rust_lang;
 use lsp::CompletionParams;
 use multi_buffer::{IndentGuide, PathKey};
 use parking_lot::Mutex;
@@ -50,7 +51,7 @@ use std::{
     iter,
     sync::atomic::{self, AtomicUsize},
 };
-use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
+use test::build_editor_with_project;
 use text::ToPoint as _;
 use unindent::Unindent;
 use util::{
@@ -12640,6 +12641,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
                 );
             }
         });
+    cx.run_until_parked();
 
     // Handle formatting requests to the language server.
     cx.lsp

crates/editor/src/hover_links.rs 🔗

@@ -1,19 +1,14 @@
 use crate::{
     Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,
-    GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, InlayId,
-    Navigated, PointForPosition, SelectPhase,
-    editor_settings::GoToDefinitionFallback,
-    hover_popover::{self, InlayHover},
+    GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind,
+    Navigated, PointForPosition, SelectPhase, editor_settings::GoToDefinitionFallback,
     scroll::ScrollAmount,
 };
 use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window, px};
 use language::{Bias, ToOffset};
 use linkify::{LinkFinder, LinkKind};
 use lsp::LanguageServerId;
-use project::{
-    HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
-    ResolveState, ResolvedPath,
-};
+use project::{InlayId, LocationLink, Project, ResolvedPath};
 use settings::Settings;
 use std::ops::Range;
 use theme::ActiveTheme as _;
@@ -138,10 +133,9 @@ impl Editor {
                 show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
             }
             None => {
-                update_inlay_link_and_hover_points(
+                self.update_inlay_link_and_hover_points(
                     snapshot,
                     point_for_position,
-                    self,
                     hovered_link_modifier,
                     modifiers.shift,
                     window,
@@ -283,182 +277,6 @@ impl Editor {
     }
 }
 
-pub fn update_inlay_link_and_hover_points(
-    snapshot: &EditorSnapshot,
-    point_for_position: PointForPosition,
-    editor: &mut Editor,
-    secondary_held: bool,
-    shift_held: bool,
-    window: &mut Window,
-    cx: &mut Context<Editor>,
-) {
-    let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
-        Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
-    } else {
-        None
-    };
-    let mut go_to_definition_updated = false;
-    let mut hover_updated = false;
-    if let Some(hovered_offset) = hovered_offset {
-        let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
-        let previous_valid_anchor =
-            buffer_snapshot.anchor_before(point_for_position.previous_valid.to_point(snapshot));
-        let next_valid_anchor =
-            buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
-        if let Some(hovered_hint) = editor
-            .visible_inlay_hints(cx)
-            .skip_while(|hint| {
-                hint.position
-                    .cmp(&previous_valid_anchor, &buffer_snapshot)
-                    .is_lt()
-            })
-            .take_while(|hint| {
-                hint.position
-                    .cmp(&next_valid_anchor, &buffer_snapshot)
-                    .is_le()
-            })
-            .max_by_key(|hint| hint.id)
-        {
-            let inlay_hint_cache = editor.inlay_hint_cache();
-            let excerpt_id = previous_valid_anchor.excerpt_id;
-            if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
-                match cached_hint.resolve_state {
-                    ResolveState::CanResolve(_, _) => {
-                        if let Some(buffer_id) = snapshot
-                            .buffer_snapshot()
-                            .buffer_id_for_anchor(previous_valid_anchor)
-                        {
-                            inlay_hint_cache.spawn_hint_resolve(
-                                buffer_id,
-                                excerpt_id,
-                                hovered_hint.id,
-                                window,
-                                cx,
-                            );
-                        }
-                    }
-                    ResolveState::Resolved => {
-                        let mut extra_shift_left = 0;
-                        let mut extra_shift_right = 0;
-                        if cached_hint.padding_left {
-                            extra_shift_left += 1;
-                            extra_shift_right += 1;
-                        }
-                        if cached_hint.padding_right {
-                            extra_shift_right += 1;
-                        }
-                        match cached_hint.label {
-                            project::InlayHintLabel::String(_) => {
-                                if let Some(tooltip) = cached_hint.tooltip {
-                                    hover_popover::hover_at_inlay(
-                                        editor,
-                                        InlayHover {
-                                            tooltip: match tooltip {
-                                                InlayHintTooltip::String(text) => HoverBlock {
-                                                    text,
-                                                    kind: HoverBlockKind::PlainText,
-                                                },
-                                                InlayHintTooltip::MarkupContent(content) => {
-                                                    HoverBlock {
-                                                        text: content.value,
-                                                        kind: content.kind,
-                                                    }
-                                                }
-                                            },
-                                            range: InlayHighlight {
-                                                inlay: hovered_hint.id,
-                                                inlay_position: hovered_hint.position,
-                                                range: extra_shift_left
-                                                    ..hovered_hint.text().len() + extra_shift_right,
-                                            },
-                                        },
-                                        window,
-                                        cx,
-                                    );
-                                    hover_updated = true;
-                                }
-                            }
-                            project::InlayHintLabel::LabelParts(label_parts) => {
-                                let hint_start =
-                                    snapshot.anchor_to_inlay_offset(hovered_hint.position);
-                                if let Some((hovered_hint_part, part_range)) =
-                                    hover_popover::find_hovered_hint_part(
-                                        label_parts,
-                                        hint_start,
-                                        hovered_offset,
-                                    )
-                                {
-                                    let highlight_start =
-                                        (part_range.start - hint_start).0 + extra_shift_left;
-                                    let highlight_end =
-                                        (part_range.end - hint_start).0 + extra_shift_right;
-                                    let highlight = InlayHighlight {
-                                        inlay: hovered_hint.id,
-                                        inlay_position: hovered_hint.position,
-                                        range: highlight_start..highlight_end,
-                                    };
-                                    if let Some(tooltip) = hovered_hint_part.tooltip {
-                                        hover_popover::hover_at_inlay(
-                                            editor,
-                                            InlayHover {
-                                                tooltip: match tooltip {
-                                                    InlayHintLabelPartTooltip::String(text) => {
-                                                        HoverBlock {
-                                                            text,
-                                                            kind: HoverBlockKind::PlainText,
-                                                        }
-                                                    }
-                                                    InlayHintLabelPartTooltip::MarkupContent(
-                                                        content,
-                                                    ) => HoverBlock {
-                                                        text: content.value,
-                                                        kind: content.kind,
-                                                    },
-                                                },
-                                                range: highlight.clone(),
-                                            },
-                                            window,
-                                            cx,
-                                        );
-                                        hover_updated = true;
-                                    }
-                                    if let Some((language_server_id, location)) =
-                                        hovered_hint_part.location
-                                        && secondary_held
-                                        && !editor.has_pending_nonempty_selection()
-                                    {
-                                        go_to_definition_updated = true;
-                                        show_link_definition(
-                                            shift_held,
-                                            editor,
-                                            TriggerPoint::InlayHint(
-                                                highlight,
-                                                location,
-                                                language_server_id,
-                                            ),
-                                            snapshot,
-                                            window,
-                                            cx,
-                                        );
-                                    }
-                                }
-                            }
-                        };
-                    }
-                    ResolveState::Resolving => {}
-                }
-            }
-        }
-    }
-
-    if !go_to_definition_updated {
-        editor.hide_hovered_link(cx)
-    }
-    if !hover_updated {
-        hover_popover::hover_at(editor, None, window, cx);
-    }
-}
-
 pub fn show_link_definition(
     shift_held: bool,
     editor: &mut Editor,
@@ -912,7 +730,7 @@ mod tests {
         DisplayPoint,
         display_map::ToDisplayPoint,
         editor_tests::init_test,
-        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
         test::editor_lsp_test_context::EditorLspTestContext,
     };
     use futures::StreamExt;
@@ -1343,7 +1161,7 @@ mod tests {
         cx.background_executor.run_until_parked();
         cx.update_editor(|editor, _window, cx| {
             let expected_layers = vec![hint_label.to_string()];
-            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
         });
 

crates/editor/src/hover_popover.rs 🔗

@@ -986,17 +986,17 @@ impl DiagnosticPopover {
 mod tests {
     use super::*;
     use crate::{
-        InlayId, PointForPosition,
+        PointForPosition,
         actions::ConfirmCompletion,
         editor_tests::{handle_completion_request, init_test},
-        hover_links::update_inlay_link_and_hover_points,
-        inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+        inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels},
         test::editor_lsp_test_context::EditorLspTestContext,
     };
     use collections::BTreeSet;
     use gpui::App;
     use indoc::indoc;
     use markdown::parser::MarkdownEvent;
+    use project::InlayId;
     use settings::InlayHintSettingsContent;
     use smol::stream::StreamExt;
     use std::sync::atomic;
@@ -1648,7 +1648,7 @@ mod tests {
         cx.background_executor.run_until_parked();
         cx.update_editor(|editor, _, cx| {
             let expected_layers = vec![entire_hint_label.to_string()];
-            assert_eq!(expected_layers, cached_hint_labels(editor));
+            assert_eq!(expected_layers, cached_hint_labels(editor, cx));
             assert_eq!(expected_layers, visible_hint_labels(editor, cx));
         });
 
@@ -1687,10 +1687,9 @@ mod tests {
             }
         });
         cx.update_editor(|editor, window, cx| {
-            update_inlay_link_and_hover_points(
+            editor.update_inlay_link_and_hover_points(
                 &editor.snapshot(window, cx),
                 new_type_hint_part_hover_position,
-                editor,
                 true,
                 false,
                 window,
@@ -1758,10 +1757,9 @@ mod tests {
         cx.background_executor.run_until_parked();
 
         cx.update_editor(|editor, window, cx| {
-            update_inlay_link_and_hover_points(
+            editor.update_inlay_link_and_hover_points(
                 &editor.snapshot(window, cx),
                 new_type_hint_part_hover_position,
-                editor,
                 true,
                 false,
                 window,
@@ -1813,10 +1811,9 @@ mod tests {
             }
         });
         cx.update_editor(|editor, window, cx| {
-            update_inlay_link_and_hover_points(
+            editor.update_inlay_link_and_hover_points(
                 &editor.snapshot(window, cx),
                 struct_hint_part_hover_position,
-                editor,
                 true,
                 false,
                 window,

crates/editor/src/inlays.rs 🔗

@@ -0,0 +1,193 @@
+//! The logic, responsible for managing [`Inlay`]s in the editor.
+//!
+//! Inlays are "not real" text that gets mixed into the "real" buffer's text.
+//! They are attached to a certain [`Anchor`], and display certain contents (usually, strings)
+//! between real text around that anchor.
+//!
+//! Inlay examples in Zed:
+//! * inlay hints, received from LSP
+//! * inline values, shown in the debugger
+//! * inline predictions, showing the Zeta/Copilot/etc. predictions
+//! * document color values, if configured to be displayed as inlays
+//! * ... anything else, potentially.
+//!
+//! Editor uses [`crate::DisplayMap`] and [`crate::display_map::InlayMap`] to manage what's rendered inside the editor, using
+//! [`InlaySplice`] to update this state.
+
+/// Logic, related to managing LSP inlay hint inlays.
+pub mod inlay_hints;
+
+use std::{any::TypeId, sync::OnceLock};
+
+use gpui::{Context, HighlightStyle, Hsla, Rgba, Task};
+use multi_buffer::Anchor;
+use project::{InlayHint, InlayId};
+use text::Rope;
+
+use crate::{Editor, hover_links::InlayHighlight};
+
+/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
+/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
+/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
+/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+    pub to_remove: Vec<InlayId>,
+    pub to_insert: Vec<Inlay>,
+}
+
+impl InlaySplice {
+    pub fn is_empty(&self) -> bool {
+        self.to_remove.is_empty() && self.to_insert.is_empty()
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+    pub id: InlayId,
+    pub position: Anchor,
+    pub content: InlayContent,
+}
+
+#[derive(Debug, Clone)]
+pub enum InlayContent {
+    Text(text::Rope),
+    Color(Hsla),
+}
+
+impl Inlay {
+    pub fn hint(id: InlayId, position: Anchor, hint: &InlayHint) -> Self {
+        let mut text = hint.text();
+        if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
+            text.push(" ");
+        }
+        if hint.padding_left && text.chars_at(0).next() != Some(' ') {
+            text.push_front(" ");
+        }
+        Self {
+            id,
+            position,
+            content: InlayContent::Text(text),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn mock_hint(id: usize, position: Anchor, text: impl Into<Rope>) -> Self {
+        Self {
+            id: InlayId::Hint(id),
+            position,
+            content: InlayContent::Text(text.into()),
+        }
+    }
+
+    pub fn color(id: usize, position: Anchor, color: Rgba) -> Self {
+        Self {
+            id: InlayId::Color(id),
+            position,
+            content: InlayContent::Color(color.into()),
+        }
+    }
+
+    pub fn edit_prediction<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::EditPrediction(id),
+            position,
+            content: InlayContent::Text(text.into()),
+        }
+    }
+
+    pub fn debugger<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+        Self {
+            id: InlayId::DebuggerValue(id),
+            position,
+            content: InlayContent::Text(text.into()),
+        }
+    }
+
+    pub fn text(&self) -> &Rope {
+        static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
+        match &self.content {
+            InlayContent::Text(text) => text,
+            InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")),
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn get_color(&self) -> Option<Hsla> {
+        match self.content {
+            InlayContent::Color(color) => Some(color),
+            _ => None,
+        }
+    }
+}
+
+pub struct InlineValueCache {
+    pub enabled: bool,
+    pub inlays: Vec<InlayId>,
+    pub refresh_task: Task<Option<()>>,
+}
+
+impl InlineValueCache {
+    pub fn new(enabled: bool) -> Self {
+        Self {
+            enabled,
+            inlays: Vec::new(),
+            refresh_task: Task::ready(None),
+        }
+    }
+}
+
+impl Editor {
+    /// Modify which hints are displayed in the editor.
+    pub fn splice_inlays(
+        &mut self,
+        to_remove: &[InlayId],
+        to_insert: Vec<Inlay>,
+        cx: &mut Context<Self>,
+    ) {
+        if let Some(inlay_hints) = &mut self.inlay_hints {
+            for id_to_remove in to_remove {
+                inlay_hints.added_hints.remove(id_to_remove);
+            }
+        }
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.splice_inlays(to_remove, to_insert, cx)
+        });
+        cx.notify();
+    }
+
+    pub(crate) fn highlight_inlays<T: 'static>(
+        &mut self,
+        highlights: Vec<InlayHighlight>,
+        style: HighlightStyle,
+        cx: &mut Context<Self>,
+    ) {
+        self.display_map.update(cx, |map, _| {
+            map.highlight_inlays(TypeId::of::<T>(), highlights, style)
+        });
+        cx.notify();
+    }
+
+    pub fn inline_values_enabled(&self) -> bool {
+        self.inline_value_cache.enabled
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn inline_value_inlays(&self, cx: &gpui::App) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_)))
+            .cloned()
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn all_inlays(&self, cx: &gpui::App) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .cloned()
+            .collect()
+    }
+}

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

@@ -1,295 +1,116 @@
-/// Stores and updates all data received from LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint">textDocument/inlayHint</a> requests.
-/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere.
-/// On every update, cache may query for more inlay hints and update inlays on the screen.
-///
-/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map.
-/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work.
-///
-/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes.
 use std::{
-    cmp,
+    collections::hash_map,
     ops::{ControlFlow, Range},
     sync::Arc,
     time::Duration,
 };
 
-use crate::{
-    Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay,
-};
-use anyhow::Context as _;
 use clock::Global;
-use futures::future;
-use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
+use collections::{HashMap, HashSet};
+use futures::future::join_all;
+use gpui::{App, Entity, Task};
 use language::{
-    Buffer, BufferSnapshot,
-    language_settings::{InlayHintKind, InlayHintSettings},
+    BufferRow,
+    language_settings::{InlayHintKind, InlayHintSettings, language_settings},
 };
-use parking_lot::RwLock;
-use project::{InlayHint, ResolveState};
+use lsp::LanguageServerId;
+use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot};
+use parking_lot::Mutex;
+use project::{
+    HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip,
+    InvalidationStrategy, ResolveState,
+    lsp_store::{CacheInlayHints, ResolvedHint},
+};
+use text::{Bias, BufferId};
+use ui::{Context, Window};
+use util::debug_panic;
 
-use collections::{HashMap, HashSet, hash_map};
-use smol::lock::Semaphore;
-use sum_tree::Bias;
-use text::{BufferId, ToOffset, ToPoint};
-use util::{ResultExt, post_inc};
+use super::{Inlay, InlayId};
+use crate::{
+    Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value,
+    hover_links::{InlayHighlight, TriggerPoint, show_link_definition},
+    hover_popover::{self, InlayHover},
+    inlays::InlaySplice,
+};
 
-pub struct InlayHintCache {
-    hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
-    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
-    version: usize,
-    pub(super) enabled: bool,
+pub fn inlay_hint_settings(
+    location: Anchor,
+    snapshot: &MultiBufferSnapshot,
+    cx: &mut Context<Editor>,
+) -> InlayHintSettings {
+    let file = snapshot.file_at(location);
+    let language = snapshot.language_at(location).map(|l| l.name());
+    language_settings(language, file, cx).inlay_hints
+}
+
+#[derive(Debug)]
+pub struct LspInlayHintData {
+    enabled: bool,
     modifiers_override: bool,
     enabled_in_settings: bool,
-    update_tasks: HashMap<ExcerptId, TasksForRanges>,
-    refresh_task: Task<()>,
+    allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
     invalidate_debounce: Option<Duration>,
     append_debounce: Option<Duration>,
-    lsp_request_limiter: Arc<Semaphore>,
-}
-
-#[derive(Debug)]
-struct TasksForRanges {
-    tasks: Vec<Task<()>>,
-    sorted_ranges: Vec<Range<language::Anchor>>,
-}
-
-#[derive(Debug)]
-struct CachedExcerptHints {
-    version: usize,
-    buffer_version: Global,
-    buffer_id: BufferId,
-    ordered_hints: Vec<InlayId>,
-    hints_by_id: HashMap<InlayId, InlayHint>,
-}
-
-/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts.
-#[derive(Debug, Clone, Copy)]
-pub(super) enum InvalidationStrategy {
-    /// Hints reset is <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">requested</a> by the LSP server.
-    /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
-    ///
-    /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise.
-    RefreshRequested,
-    /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
-    /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence.
-    BufferEdited,
-    /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
-    /// No invalidation should be done at all, all new hints are added to the cache.
-    ///
-    /// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other).
-    /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen.
-    None,
-}
-
-/// A splice to send into the `inlay_map` for updating the visible inlays on the screen.
-/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
-/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
-/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
-#[derive(Debug, Default)]
-pub(super) struct InlaySplice {
-    pub to_remove: Vec<InlayId>,
-    pub to_insert: Vec<Inlay>,
-}
-
-#[derive(Debug)]
-struct ExcerptHintsUpdate {
-    excerpt_id: ExcerptId,
-    remove_from_visible: HashSet<InlayId>,
-    remove_from_cache: HashSet<InlayId>,
-    add_to_cache: Vec<InlayHint>,
-}
-
-#[derive(Debug, Clone, Copy)]
-struct ExcerptQuery {
-    buffer_id: BufferId,
-    excerpt_id: ExcerptId,
-    cache_version: usize,
-    invalidate: InvalidationStrategy,
-    reason: &'static str,
-}
-
-impl InvalidationStrategy {
-    fn should_invalidate(&self) -> bool {
-        matches!(
-            self,
-            InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
-        )
-    }
+    hint_refresh_tasks: HashMap<BufferId, HashMap<Vec<Range<BufferRow>>, Vec<Task<()>>>>,
+    hint_chunk_fetched: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
+    pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
 }
 
-impl TasksForRanges {
-    fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
+impl LspInlayHintData {
+    pub fn new(settings: InlayHintSettings) -> Self {
         Self {
-            tasks: vec![task],
-            sorted_ranges: query_ranges.into_sorted_query_ranges(),
+            modifiers_override: false,
+            enabled: settings.enabled,
+            enabled_in_settings: settings.enabled,
+            hint_refresh_tasks: HashMap::default(),
+            added_hints: HashMap::default(),
+            hint_chunk_fetched: HashMap::default(),
+            invalidate_debounce: debounce_value(settings.edit_debounce_ms),
+            append_debounce: debounce_value(settings.scroll_debounce_ms),
+            allowed_hint_kinds: settings.enabled_inlay_hint_kinds(),
         }
     }
 
-    fn update_cached_tasks(
-        &mut self,
-        buffer_snapshot: &BufferSnapshot,
-        query_ranges: QueryRanges,
-        invalidate: InvalidationStrategy,
-        spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
-    ) {
-        let query_ranges = if invalidate.should_invalidate() {
-            self.tasks.clear();
-            self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges();
-            query_ranges
+    pub fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
+        if self.modifiers_override == new_override {
+            return None;
+        }
+        self.modifiers_override = new_override;
+        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
+        {
+            self.clear();
+            Some(false)
         } else {
-            let mut non_cached_query_ranges = query_ranges;
-            non_cached_query_ranges.before_visible = non_cached_query_ranges
-                .before_visible
-                .into_iter()
-                .flat_map(|query_range| {
-                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
-                })
-                .collect();
-            non_cached_query_ranges.visible = non_cached_query_ranges
-                .visible
-                .into_iter()
-                .flat_map(|query_range| {
-                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
-                })
-                .collect();
-            non_cached_query_ranges.after_visible = non_cached_query_ranges
-                .after_visible
-                .into_iter()
-                .flat_map(|query_range| {
-                    self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
-                })
-                .collect();
-            non_cached_query_ranges
-        };
-
-        if !query_ranges.is_empty() {
-            self.tasks.push(spawn_task(query_ranges));
+            Some(true)
         }
     }
 
-    fn remove_cached_ranges_from_query(
-        &mut self,
-        buffer_snapshot: &BufferSnapshot,
-        query_range: Range<language::Anchor>,
-    ) -> Vec<Range<language::Anchor>> {
-        let mut ranges_to_query = Vec::new();
-        let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
-        for cached_range in self
-            .sorted_ranges
-            .iter_mut()
-            .skip_while(|cached_range| {
-                cached_range
-                    .end
-                    .cmp(&query_range.start, buffer_snapshot)
-                    .is_lt()
-            })
-            .take_while(|cached_range| {
-                cached_range
-                    .start
-                    .cmp(&query_range.end, buffer_snapshot)
-                    .is_le()
-            })
-        {
-            match latest_cached_range {
-                Some(latest_cached_range) => {
-                    if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
-                    {
-                        ranges_to_query.push(latest_cached_range.end..cached_range.start);
-                        cached_range.start = latest_cached_range.end;
-                    }
-                }
-                None => {
-                    if query_range
-                        .start
-                        .cmp(&cached_range.start, buffer_snapshot)
-                        .is_lt()
-                    {
-                        ranges_to_query.push(query_range.start..cached_range.start);
-                        cached_range.start = query_range.start;
-                    }
-                }
-            }
-            latest_cached_range = Some(cached_range);
+    pub fn toggle(&mut self, enabled: bool) -> bool {
+        if self.enabled == enabled {
+            return false;
         }
-
-        match latest_cached_range {
-            Some(latest_cached_range) => {
-                if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
-                    ranges_to_query.push(latest_cached_range.end..query_range.end);
-                    latest_cached_range.end = query_range.end;
-                }
-            }
-            None => {
-                ranges_to_query.push(query_range.clone());
-                self.sorted_ranges.push(query_range);
-                self.sorted_ranges
-                    .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
-            }
+        self.enabled = enabled;
+        self.modifiers_override = false;
+        if !enabled {
+            self.clear();
         }
-
-        ranges_to_query
-    }
-
-    fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
-        self.sorted_ranges = self
-            .sorted_ranges
-            .drain(..)
-            .filter_map(|mut cached_range| {
-                if cached_range.start.cmp(&range.end, buffer).is_gt()
-                    || cached_range.end.cmp(&range.start, buffer).is_lt()
-                {
-                    Some(vec![cached_range])
-                } else if cached_range.start.cmp(&range.start, buffer).is_ge()
-                    && cached_range.end.cmp(&range.end, buffer).is_le()
-                {
-                    None
-                } else if range.start.cmp(&cached_range.start, buffer).is_ge()
-                    && range.end.cmp(&cached_range.end, buffer).is_le()
-                {
-                    Some(vec![
-                        cached_range.start..range.start,
-                        range.end..cached_range.end,
-                    ])
-                } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
-                    cached_range.start = range.end;
-                    Some(vec![cached_range])
-                } else {
-                    cached_range.end = range.start;
-                    Some(vec![cached_range])
-                }
-            })
-            .flatten()
-            .collect();
+        true
     }
-}
 
-impl InlayHintCache {
-    pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self {
-        Self {
-            allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
-            enabled: inlay_hint_settings.enabled,
-            modifiers_override: false,
-            enabled_in_settings: inlay_hint_settings.enabled,
-            hints: HashMap::default(),
-            update_tasks: HashMap::default(),
-            refresh_task: Task::ready(()),
-            invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms),
-            append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms),
-            version: 0,
-            lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
-        }
+    pub fn clear(&mut self) {
+        self.hint_refresh_tasks.clear();
+        self.hint_chunk_fetched.clear();
+        self.added_hints.clear();
     }
 
     /// Checks inlay hint settings for enabled hint kinds and general enabled state.
     /// Generates corresponding inlay_map splice updates on settings changes.
     /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries.
-    pub(super) fn update_settings(
+    fn update_settings(
         &mut self,
-        multi_buffer: &Entity<MultiBuffer>,
         new_hint_settings: InlayHintSettings,
         visible_hints: Vec<Inlay>,
-        cx: &mut Context<Editor>,
-    ) -> ControlFlow<Option<InlaySplice>> {
+    ) -> ControlFlow<Option<InlaySplice>, Option<InlaySplice>> {
         let old_enabled = self.enabled;
         // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
         // hint visibility changes when other settings change (such as theme).
@@ -314,23 +135,30 @@ impl InlayHintCache {
                 if new_allowed_hint_kinds == self.allowed_hint_kinds {
                     ControlFlow::Break(None)
                 } else {
-                    let new_splice = self.new_allowed_hint_kinds_splice(
-                        multi_buffer,
-                        &visible_hints,
-                        &new_allowed_hint_kinds,
-                        cx,
-                    );
-                    if new_splice.is_some() {
-                        self.version += 1;
-                        self.allowed_hint_kinds = new_allowed_hint_kinds;
-                    }
-                    ControlFlow::Break(new_splice)
+                    self.allowed_hint_kinds = new_allowed_hint_kinds;
+                    ControlFlow::Continue(
+                        Some(InlaySplice {
+                            to_remove: visible_hints
+                                .iter()
+                                .filter_map(|inlay| {
+                                    let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
+                                    if !self.allowed_hint_kinds.contains(&inlay_kind) {
+                                        Some(inlay.id)
+                                    } else {
+                                        None
+                                    }
+                                })
+                                .collect(),
+                            to_insert: Vec::new(),
+                        })
+                        .filter(|splice| !splice.is_empty()),
+                    )
                 }
             }
             (true, false) => {
                 self.modifiers_override = false;
                 self.allowed_hint_kinds = new_allowed_hint_kinds;
-                if self.hints.is_empty() {
+                if visible_hints.is_empty() {
                     ControlFlow::Break(None)
                 } else {
                     self.clear();
@@ -343,978 +171,770 @@ impl InlayHintCache {
             (false, true) => {
                 self.modifiers_override = false;
                 self.allowed_hint_kinds = new_allowed_hint_kinds;
-                ControlFlow::Continue(())
+                ControlFlow::Continue(
+                    Some(InlaySplice {
+                        to_remove: visible_hints
+                            .iter()
+                            .filter_map(|inlay| {
+                                let inlay_kind = self.added_hints.get(&inlay.id).copied()?;
+                                if !self.allowed_hint_kinds.contains(&inlay_kind) {
+                                    Some(inlay.id)
+                                } else {
+                                    None
+                                }
+                            })
+                            .collect(),
+                        to_insert: Vec::new(),
+                    })
+                    .filter(|splice| !splice.is_empty()),
+                )
             }
         }
     }
 
-    pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option<bool> {
-        if self.modifiers_override == new_override {
-            return None;
-        }
-        self.modifiers_override = new_override;
-        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
-        {
-            self.clear();
-            Some(false)
-        } else {
-            Some(true)
+    pub(crate) fn remove_inlay_chunk_data<'a>(
+        &'a mut self,
+        removed_buffer_ids: impl IntoIterator<Item = &'a BufferId> + 'a,
+    ) {
+        for buffer_id in removed_buffer_ids {
+            self.hint_refresh_tasks.remove(buffer_id);
+            self.hint_chunk_fetched.remove(buffer_id);
         }
     }
+}
 
-    pub(super) fn toggle(&mut self, enabled: bool) -> bool {
-        if self.enabled == enabled {
+#[derive(Debug, Clone)]
+pub enum InlayHintRefreshReason {
+    ModifiersChanged(bool),
+    Toggle(bool),
+    SettingsChange(InlayHintSettings),
+    NewLinesShown,
+    BufferEdited(BufferId),
+    RefreshRequested(LanguageServerId),
+    ExcerptsRemoved(Vec<ExcerptId>),
+}
+
+impl Editor {
+    pub fn supports_inlay_hints(&self, cx: &mut App) -> bool {
+        let Some(provider) = self.semantics_provider.as_ref() else {
             return false;
-        }
-        self.enabled = enabled;
-        self.modifiers_override = false;
-        if !enabled {
-            self.clear();
-        }
-        true
+        };
+
+        let mut supports = false;
+        self.buffer().update(cx, |this, cx| {
+            this.for_each_buffer(|buffer| {
+                supports |= provider.supports_inlay_hints(buffer, cx);
+            });
+        });
+
+        supports
     }
 
-    /// If needed, queries LSP for new inlay hints, using the invalidation strategy given.
-    /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first,
-    /// followed by the delayed queries of the same range above and below the visible one.
-    /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges.
-    pub(super) fn spawn_hint_refresh(
+    pub fn toggle_inline_values(
         &mut self,
-        reason_description: &'static str,
-        excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
-        invalidate: InvalidationStrategy,
-        ignore_debounce: bool,
-        cx: &mut Context<Editor>,
-    ) -> Option<InlaySplice> {
-        if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override)
-        {
-            return None;
-        }
-        let mut invalidated_hints = Vec::new();
-        if invalidate.should_invalidate() {
-            self.update_tasks
-                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
-            self.hints.retain(|cached_excerpt, cached_hints| {
-                let retain = excerpts_to_query.contains_key(cached_excerpt);
-                if !retain {
-                    invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
-                }
-                retain
-            });
-        }
-        if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
-            return None;
-        }
+        _: &ToggleInlineValues,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.inline_value_cache.enabled = !self.inline_value_cache.enabled;
 
-        let cache_version = self.version + 1;
-        let debounce_duration = if ignore_debounce {
-            None
-        } else if invalidate.should_invalidate() {
-            self.invalidate_debounce
-        } else {
-            self.append_debounce
-        };
-        self.refresh_task = cx.spawn(async move |editor, cx| {
-            if let Some(debounce_duration) = debounce_duration {
-                cx.background_executor().timer(debounce_duration).await;
-            }
+        self.refresh_inline_values(cx);
+    }
 
-            editor
-                .update(cx, |editor, cx| {
-                    spawn_new_update_tasks(
-                        editor,
-                        reason_description,
-                        excerpts_to_query,
-                        invalidate,
-                        cache_version,
-                        cx,
-                    )
-                })
-                .ok();
-        });
+    pub fn toggle_inlay_hints(
+        &mut self,
+        _: &ToggleInlayHints,
+        _: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.refresh_inlay_hints(
+            InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()),
+            cx,
+        );
+    }
 
-        if invalidated_hints.is_empty() {
-            None
-        } else {
-            Some(InlaySplice {
-                to_remove: invalidated_hints,
-                to_insert: Vec::new(),
-            })
-        }
+    pub fn inlay_hints_enabled(&self) -> bool {
+        self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled)
     }
 
-    fn new_allowed_hint_kinds_splice(
-        &self,
-        multi_buffer: &Entity<MultiBuffer>,
-        visible_hints: &[Inlay],
-        new_kinds: &HashSet<Option<InlayHintKind>>,
-        cx: &mut Context<Editor>,
-    ) -> Option<InlaySplice> {
-        let old_kinds = &self.allowed_hint_kinds;
-        if new_kinds == old_kinds {
-            return None;
+    /// Updates inlay hints for the visible ranges of the singleton buffer(s).
+    /// Based on its parameters, either invalidates the previous data, or appends to it.
+    pub(crate) fn refresh_inlay_hints(
+        &mut self,
+        reason: InlayHintRefreshReason,
+        cx: &mut Context<Self>,
+    ) {
+        if !self.mode.is_full() || self.inlay_hints.is_none() {
+            return;
         }
+        let Some(semantics_provider) = self.semantics_provider() else {
+            return;
+        };
+        let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else {
+            return;
+        };
 
-        let mut to_remove = Vec::new();
-        let mut to_insert = Vec::new();
-        let mut shown_hints_to_remove = visible_hints.iter().fold(
-            HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
-            |mut current_hints, inlay| {
-                current_hints
-                    .entry(inlay.position.excerpt_id)
-                    .or_default()
-                    .push((inlay.position, inlay.id));
-                current_hints
-            },
-        );
+        let debounce = match &reason {
+            InlayHintRefreshReason::SettingsChange(_)
+            | InlayHintRefreshReason::Toggle(_)
+            | InlayHintRefreshReason::ExcerptsRemoved(_)
+            | InlayHintRefreshReason::ModifiersChanged(_) => None,
+            _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| {
+                if invalidate_cache.should_invalidate() {
+                    inlay_hints.invalidate_debounce
+                } else {
+                    inlay_hints.append_debounce
+                }
+            }),
+        };
 
-        let multi_buffer = multi_buffer.read(cx);
-        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
-
-        for (excerpt_id, excerpt_cached_hints) in &self.hints {
-            let shown_excerpt_hints_to_remove =
-                shown_hints_to_remove.entry(*excerpt_id).or_default();
-            let excerpt_cached_hints = excerpt_cached_hints.read();
-            let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
-            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
-                let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else {
-                    return false;
+        let mut visible_excerpts = self.visible_excerpts(cx);
+        let mut all_affected_buffers = HashSet::default();
+        let ignore_previous_fetches = match reason {
+            InlayHintRefreshReason::ModifiersChanged(_)
+            | InlayHintRefreshReason::Toggle(_)
+            | InlayHintRefreshReason::SettingsChange(_) => true,
+            InlayHintRefreshReason::NewLinesShown
+            | InlayHintRefreshReason::RefreshRequested(_)
+            | InlayHintRefreshReason::ExcerptsRemoved(_) => false,
+            InlayHintRefreshReason::BufferEdited(buffer_id) => {
+                let Some(affected_language) = self
+                    .buffer()
+                    .read(cx)
+                    .buffer(buffer_id)
+                    .and_then(|buffer| buffer.read(cx).language().cloned())
+                else {
+                    return;
                 };
-                let buffer_snapshot = buffer.read(cx).snapshot();
-                loop {
-                    match excerpt_cache.peek() {
-                        Some(&cached_hint_id) => {
-                            let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
-                            if cached_hint_id == shown_hint_id {
-                                excerpt_cache.next();
-                                return !new_kinds.contains(&cached_hint.kind);
-                            }
 
-                            match cached_hint
-                                .position
-                                .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
-                            {
-                                cmp::Ordering::Less | cmp::Ordering::Equal => {
-                                    if !old_kinds.contains(&cached_hint.kind)
-                                        && new_kinds.contains(&cached_hint.kind)
-                                        && let Some(anchor) = multi_buffer_snapshot
-                                            .anchor_in_excerpt(*excerpt_id, cached_hint.position)
-                                    {
-                                        to_insert.push(Inlay::hint(
-                                            cached_hint_id.id(),
-                                            anchor,
-                                            cached_hint,
-                                        ));
-                                    }
-                                    excerpt_cache.next();
-                                }
-                                cmp::Ordering::Greater => return true,
+                all_affected_buffers.extend(
+                    self.buffer()
+                        .read(cx)
+                        .all_buffers()
+                        .into_iter()
+                        .filter_map(|buffer| {
+                            let buffer = buffer.read(cx);
+                            if buffer.language() == Some(&affected_language) {
+                                Some(buffer.remote_id())
+                            } else {
+                                None
                             }
-                        }
-                        None => return true,
-                    }
-                }
-            });
+                        }),
+                );
 
-            for cached_hint_id in excerpt_cache {
-                let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
-                let cached_hint_kind = maybe_missed_cached_hint.kind;
-                if !old_kinds.contains(&cached_hint_kind)
-                    && new_kinds.contains(&cached_hint_kind)
-                    && let Some(anchor) = multi_buffer_snapshot
-                        .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
-                {
-                    to_insert.push(Inlay::hint(
-                        cached_hint_id.id(),
-                        anchor,
-                        maybe_missed_cached_hint,
-                    ));
-                }
+                semantics_provider.invalidate_inlay_hints(&all_affected_buffers, cx);
+                visible_excerpts.retain(|_, (visible_buffer, _, _)| {
+                    visible_buffer.read(cx).language() == Some(&affected_language)
+                });
+                false
             }
-        }
+        };
 
-        to_remove.extend(
-            shown_hints_to_remove
-                .into_values()
-                .flatten()
-                .map(|(_, hint_id)| hint_id),
-        );
-        if to_remove.is_empty() && to_insert.is_empty() {
-            None
-        } else {
-            Some(InlaySplice {
-                to_remove,
-                to_insert,
-            })
+        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
+            return;
+        };
+
+        if invalidate_cache.should_invalidate() {
+            inlay_hints.clear();
         }
-    }
 
-    /// Completely forget of certain excerpts that were removed from the multibuffer.
-    pub(super) fn remove_excerpts(
-        &mut self,
-        excerpts_removed: &[ExcerptId],
-    ) -> Option<InlaySplice> {
-        let mut to_remove = Vec::new();
-        for excerpt_to_remove in excerpts_removed {
-            self.update_tasks.remove(excerpt_to_remove);
-            if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
-                let cached_hints = cached_hints.read();
-                to_remove.extend(cached_hints.ordered_hints.iter().copied());
+        let mut buffers_to_query = HashMap::default();
+        for (excerpt_id, (buffer, buffer_version, visible_range)) in visible_excerpts {
+            let buffer_id = buffer.read(cx).remote_id();
+            if !self.registered_buffers.contains_key(&buffer_id) {
+                continue;
             }
-        }
-        if to_remove.is_empty() {
-            None
-        } else {
-            self.version += 1;
-            Some(InlaySplice {
-                to_remove,
-                to_insert: Vec::new(),
-            })
-        }
-    }
 
-    pub(super) fn clear(&mut self) {
-        if !self.update_tasks.is_empty() || !self.hints.is_empty() {
-            self.version += 1;
+            let buffer_snapshot = buffer.read(cx).snapshot();
+            let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start)
+                ..buffer_snapshot.anchor_after(visible_range.end);
+
+            let visible_excerpts =
+                buffers_to_query
+                    .entry(buffer_id)
+                    .or_insert_with(|| VisibleExcerpts {
+                        excerpts: Vec::new(),
+                        ranges: Vec::new(),
+                        buffer_version: buffer_version.clone(),
+                        buffer: buffer.clone(),
+                    });
+            visible_excerpts.buffer_version = buffer_version;
+            visible_excerpts.excerpts.push(excerpt_id);
+            visible_excerpts.ranges.push(buffer_anchor_range);
         }
-        self.update_tasks.clear();
-        self.refresh_task = Task::ready(());
-        self.hints.clear();
-    }
 
-    pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
-        self.hints
-            .get(&excerpt_id)?
-            .read()
-            .hints_by_id
-            .get(&hint_id)
-            .cloned()
-    }
+        let all_affected_buffers = Arc::new(Mutex::new(all_affected_buffers));
+        for (buffer_id, visible_excerpts) in buffers_to_query {
+            let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
+            if visible_excerpts
+                .buffer_version
+                .changed_since(&fetched_tasks.0)
+            {
+                fetched_tasks.1.clear();
+                fetched_tasks.0 = visible_excerpts.buffer_version.clone();
+                inlay_hints.hint_refresh_tasks.remove(&buffer_id);
+            }
 
-    pub fn hints(&self) -> Vec<InlayHint> {
-        let mut hints = Vec::new();
-        for excerpt_hints in self.hints.values() {
-            let excerpt_hints = excerpt_hints.read();
-            hints.extend(
-                excerpt_hints
-                    .ordered_hints
-                    .iter()
-                    .map(|id| &excerpt_hints.hints_by_id[id])
-                    .cloned(),
-            );
-        }
-        hints
-    }
+            let applicable_chunks =
+                semantics_provider.applicable_inlay_chunks(buffer_id, &visible_excerpts.ranges, cx);
 
-    /// Queries a certain hint from the cache for extra data via the LSP <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHint_resolve">resolve</a> request.
-    pub(super) fn spawn_hint_resolve(
-        &self,
-        buffer_id: BufferId,
-        excerpt_id: ExcerptId,
-        id: InlayId,
-        window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) {
-        if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
-            let mut guard = excerpt_hints.write();
-            if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
-                && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state
+            match inlay_hints
+                .hint_refresh_tasks
+                .entry(buffer_id)
+                .or_default()
+                .entry(applicable_chunks)
             {
-                let hint_to_resolve = cached_hint.clone();
-                let server_id = *server_id;
-                cached_hint.resolve_state = ResolveState::Resolving;
-                drop(guard);
-                cx.spawn_in(window, async move |editor, cx| {
-                    let resolved_hint_task = editor.update(cx, |editor, cx| {
-                        let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
-                        editor.semantics_provider.as_ref()?.resolve_inlay_hint(
-                            hint_to_resolve,
-                            buffer,
-                            server_id,
+                hash_map::Entry::Occupied(mut o) => {
+                    if invalidate_cache.should_invalidate() || ignore_previous_fetches {
+                        o.get_mut().push(spawn_editor_hints_refresh(
+                            buffer_id,
+                            invalidate_cache,
+                            ignore_previous_fetches,
+                            debounce,
+                            visible_excerpts,
+                            all_affected_buffers.clone(),
                             cx,
-                        )
-                    })?;
-                    if let Some(resolved_hint_task) = resolved_hint_task {
-                        let mut resolved_hint =
-                            resolved_hint_task.await.context("hint resolve task")?;
-                        editor.read_with(cx, |editor, _| {
-                            if let Some(excerpt_hints) =
-                                editor.inlay_hint_cache.hints.get(&excerpt_id)
-                            {
-                                let mut guard = excerpt_hints.write();
-                                if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
-                                    && cached_hint.resolve_state == ResolveState::Resolving
-                                {
-                                    resolved_hint.resolve_state = ResolveState::Resolved;
-                                    *cached_hint = resolved_hint;
-                                }
-                            }
-                        })?;
+                        ));
                     }
-
-                    anyhow::Ok(())
-                })
-                .detach_and_log_err(cx);
+                }
+                hash_map::Entry::Vacant(v) => {
+                    v.insert(Vec::new()).push(spawn_editor_hints_refresh(
+                        buffer_id,
+                        invalidate_cache,
+                        ignore_previous_fetches,
+                        debounce,
+                        visible_excerpts,
+                        all_affected_buffers.clone(),
+                        cx,
+                    ));
+                }
             }
         }
     }
-}
 
-fn debounce_value(debounce_ms: u64) -> Option<Duration> {
-    if debounce_ms > 0 {
-        Some(Duration::from_millis(debounce_ms))
-    } else {
-        None
+    pub fn clear_inlay_hints(&mut self, cx: &mut Context<Self>) {
+        let to_remove = self
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .map(|inlay| {
+                let inlay_id = inlay.id;
+                if let Some(inlay_hints) = &mut self.inlay_hints {
+                    inlay_hints.added_hints.remove(&inlay_id);
+                }
+                inlay_id
+            })
+            .collect::<Vec<_>>();
+        self.splice_inlays(&to_remove, Vec::new(), cx);
     }
-}
-
-fn spawn_new_update_tasks(
-    editor: &mut Editor,
-    reason: &'static str,
-    excerpts_to_query: HashMap<ExcerptId, (Entity<Buffer>, Global, Range<usize>)>,
-    invalidate: InvalidationStrategy,
-    update_cache_version: usize,
-    cx: &mut Context<Editor>,
-) {
-    for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
-        excerpts_to_query
-    {
-        if excerpt_visible_range.is_empty() {
-            continue;
-        }
-        let buffer = excerpt_buffer.read(cx);
-        let buffer_id = buffer.remote_id();
-        let buffer_snapshot = buffer.snapshot();
-        if buffer_snapshot
-            .version()
-            .changed_since(&new_task_buffer_version)
-        {
-            continue;
-        }
-
-        if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
-            let cached_excerpt_hints = cached_excerpt_hints.read();
-            let cached_buffer_version = &cached_excerpt_hints.buffer_version;
-            if cached_excerpt_hints.version > update_cache_version
-                || cached_buffer_version.changed_since(&new_task_buffer_version)
-            {
-                continue;
-            }
-        };
 
-        let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| {
-            determine_query_ranges(
-                multi_buffer,
-                excerpt_id,
-                &excerpt_buffer,
-                excerpt_visible_range,
-                cx,
-            )
-        }) else {
-            return;
-        };
-        let query = ExcerptQuery {
-            buffer_id,
-            excerpt_id,
-            cache_version: update_cache_version,
-            invalidate,
-            reason,
+    fn refresh_editor_data(
+        &mut self,
+        reason: &InlayHintRefreshReason,
+        cx: &mut Context<'_, Editor>,
+    ) -> Option<InvalidationStrategy> {
+        let visible_inlay_hints = self.visible_inlay_hints(cx);
+        let Some(inlay_hints) = self.inlay_hints.as_mut() else {
+            return None;
         };
 
-        let mut new_update_task =
-            |query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx);
-
-        match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
-            hash_map::Entry::Occupied(mut o) => {
-                o.get_mut().update_cached_tasks(
-                    &buffer_snapshot,
-                    query_ranges,
-                    invalidate,
-                    new_update_task,
-                );
-            }
-            hash_map::Entry::Vacant(v) => {
-                v.insert(TasksForRanges::new(
-                    query_ranges.clone(),
-                    new_update_task(query_ranges),
-                ));
+        let invalidate_cache = match reason {
+            InlayHintRefreshReason::ModifiersChanged(enabled) => {
+                match inlay_hints.modifiers_override(*enabled) {
+                    Some(enabled) => {
+                        if enabled {
+                            InvalidationStrategy::None
+                        } else {
+                            self.clear_inlay_hints(cx);
+                            return None;
+                        }
+                    }
+                    None => return None,
+                }
             }
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-struct QueryRanges {
-    before_visible: Vec<Range<language::Anchor>>,
-    visible: Vec<Range<language::Anchor>>,
-    after_visible: Vec<Range<language::Anchor>>,
-}
-
-impl QueryRanges {
-    fn is_empty(&self) -> bool {
-        self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty()
-    }
-
-    fn into_sorted_query_ranges(self) -> Vec<Range<text::Anchor>> {
-        let mut sorted_ranges = Vec::with_capacity(
-            self.before_visible.len() + self.visible.len() + self.after_visible.len(),
-        );
-        sorted_ranges.extend(self.before_visible);
-        sorted_ranges.extend(self.visible);
-        sorted_ranges.extend(self.after_visible);
-        sorted_ranges
-    }
-}
-
-fn determine_query_ranges(
-    multi_buffer: &mut MultiBuffer,
-    excerpt_id: ExcerptId,
-    excerpt_buffer: &Entity<Buffer>,
-    excerpt_visible_range: Range<usize>,
-    cx: &mut Context<MultiBuffer>,
-) -> Option<QueryRanges> {
-    let buffer = excerpt_buffer.read(cx);
-    let full_excerpt_range = multi_buffer
-        .excerpts_for_buffer(buffer.remote_id(), cx)
-        .into_iter()
-        .find(|(id, _)| id == &excerpt_id)
-        .map(|(_, range)| range.context)?;
-    let snapshot = buffer.snapshot();
-    let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
-
-    let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end {
-        return None;
-    } else {
-        vec![
-            buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left))
-                ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)),
-        ]
-    };
-
-    let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot);
-    let after_visible_range_start = excerpt_visible_range
-        .end
-        .saturating_add(1)
-        .min(full_excerpt_range_end_offset)
-        .min(buffer.len());
-    let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset {
-        Vec::new()
-    } else {
-        let after_range_end_offset = after_visible_range_start
-            .saturating_add(excerpt_visible_len)
-            .min(full_excerpt_range_end_offset)
-            .min(buffer.len());
-        vec![
-            buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left))
-                ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)),
-        ]
-    };
-
-    let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot);
-    let before_visible_range_end = excerpt_visible_range
-        .start
-        .saturating_sub(1)
-        .max(full_excerpt_range_start_offset);
-    let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset {
-        Vec::new()
-    } else {
-        let before_range_start_offset = before_visible_range_end
-            .saturating_sub(excerpt_visible_len)
-            .max(full_excerpt_range_start_offset);
-        vec![
-            buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left))
-                ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)),
-        ]
-    };
-
-    Some(QueryRanges {
-        before_visible: before_visible_range,
-        visible: visible_range,
-        after_visible: after_visible_range,
-    })
-}
-
-const MAX_CONCURRENT_LSP_REQUESTS: usize = 5;
-const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
-
-fn new_update_task(
-    query: ExcerptQuery,
-    query_ranges: QueryRanges,
-    excerpt_buffer: Entity<Buffer>,
-    cx: &mut Context<Editor>,
-) -> Task<()> {
-    cx.spawn(async move |editor, cx| {
-        let visible_range_update_results = future::join_all(
-            query_ranges
-                .visible
-                .into_iter()
-                .filter_map(|visible_range| {
-                    let fetch_task = editor
-                        .update(cx, |_, cx| {
-                            fetch_and_update_hints(
-                                excerpt_buffer.clone(),
-                                query,
-                                visible_range.clone(),
-                                query.invalidate.should_invalidate(),
-                                cx,
-                            )
-                        })
-                        .log_err()?;
-                    Some(async move { (visible_range, fetch_task.await) })
-                }),
-        )
-        .await;
-
-        let hint_delay = cx.background_executor().timer(Duration::from_millis(
-            INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
-        ));
-
-        let query_range_failed =
-            |range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncApp| {
-                log::error!("inlay hint update task for range failed: {e:#?}");
-                editor
-                    .update(cx, |editor, cx| {
-                        if let Some(task_ranges) = editor
-                            .inlay_hint_cache
-                            .update_tasks
-                            .get_mut(&query.excerpt_id)
+            InlayHintRefreshReason::Toggle(enabled) => {
+                if inlay_hints.toggle(*enabled) {
+                    if *enabled {
+                        InvalidationStrategy::None
+                    } else {
+                        self.clear_inlay_hints(cx);
+                        return None;
+                    }
+                } else {
+                    return None;
+                }
+            }
+            InlayHintRefreshReason::SettingsChange(new_settings) => {
+                match inlay_hints.update_settings(*new_settings, visible_inlay_hints) {
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove,
+                        to_insert,
+                    })) => {
+                        self.splice_inlays(&to_remove, to_insert, cx);
+                        return None;
+                    }
+                    ControlFlow::Break(None) => return None,
+                    ControlFlow::Continue(splice) => {
+                        if let Some(InlaySplice {
+                            to_remove,
+                            to_insert,
+                        }) = splice
                         {
-                            let buffer_snapshot = excerpt_buffer.read(cx).snapshot();
-                            task_ranges.invalidate_range(&buffer_snapshot, range);
+                            self.splice_inlays(&to_remove, to_insert, cx);
                         }
-                    })
-                    .ok()
-            };
-
-        for (range, result) in visible_range_update_results {
-            if let Err(e) = result {
-                query_range_failed(&range, e, cx);
+                        InvalidationStrategy::None
+                    }
+                }
             }
-        }
-
-        hint_delay.await;
-        let invisible_range_update_results = future::join_all(
-            query_ranges
-                .before_visible
-                .into_iter()
-                .chain(query_ranges.after_visible.into_iter())
-                .filter_map(|invisible_range| {
-                    let fetch_task = editor
-                        .update(cx, |_, cx| {
-                            fetch_and_update_hints(
-                                excerpt_buffer.clone(),
-                                query,
-                                invisible_range.clone(),
-                                false, // visible screen request already invalidated the entries
-                                cx,
-                            )
-                        })
-                        .log_err()?;
-                    Some(async move { (invisible_range, fetch_task.await) })
-                }),
-        )
-        .await;
-        for (range, result) in invisible_range_update_results {
-            if let Err(e) = result {
-                query_range_failed(&range, e, cx);
+            InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
+                let to_remove = self
+                    .display_map
+                    .read(cx)
+                    .current_inlays()
+                    .filter_map(|inlay| {
+                        if excerpts_removed.contains(&inlay.position.excerpt_id) {
+                            Some(inlay.id)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect::<Vec<_>>();
+                self.splice_inlays(&to_remove, Vec::new(), cx);
+                return None;
             }
-        }
-    })
-}
-
-fn fetch_and_update_hints(
-    excerpt_buffer: Entity<Buffer>,
-    query: ExcerptQuery,
-    fetch_range: Range<language::Anchor>,
-    invalidate: bool,
-    cx: &mut Context<Editor>,
-) -> Task<anyhow::Result<()>> {
-    cx.spawn(async move |editor, cx|{
-        let buffer_snapshot = excerpt_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-        let (lsp_request_limiter, multi_buffer_snapshot) =
-            editor.update(cx, |editor, cx| {
-                let multi_buffer_snapshot =
-                    editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-                let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
-                (lsp_request_limiter, multi_buffer_snapshot)
-            })?;
-
-        let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
-            (None, false)
-        } else {
-            match lsp_request_limiter.try_acquire() {
-                Some(guard) => (Some(guard), false),
-                None => (Some(lsp_request_limiter.acquire().await), true),
+            InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None,
+            InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
+            InlayHintRefreshReason::RefreshRequested(server_id) => {
+                InvalidationStrategy::RefreshRequested(*server_id)
             }
         };
-        let fetch_range_to_log = fetch_range.start.to_point(&buffer_snapshot)
-            ..fetch_range.end.to_point(&buffer_snapshot);
-        let inlay_hints_fetch_task = editor
-            .update(cx, |editor, cx| {
-                if got_throttled {
-                    let query_not_around_visible_range = match editor
-                        .visible_excerpts(None, cx)
-                        .remove(&query.excerpt_id)
-                    {
-                        Some((_, _, current_visible_range)) => {
-                            let visible_offset_length = current_visible_range.len();
-                            let double_visible_range = current_visible_range
-                                .start
-                                .saturating_sub(visible_offset_length)
-                                ..current_visible_range
-                                    .end
-                                    .saturating_add(visible_offset_length)
-                                    .min(buffer_snapshot.len());
-                            !double_visible_range
-                                .contains(&fetch_range.start.to_offset(&buffer_snapshot))
-                                && !double_visible_range
-                                    .contains(&fetch_range.end.to_offset(&buffer_snapshot))
-                        }
-                        None => true,
-                    };
-                    if query_not_around_visible_range {
-                        log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
-                        if let Some(task_ranges) = editor
-                            .inlay_hint_cache
-                            .update_tasks
-                            .get_mut(&query.excerpt_id)
-                        {
-                            task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
-                        }
-                        return None;
-                    }
-                }
 
-                let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
+        match &mut self.inlay_hints {
+            Some(inlay_hints) => {
+                if !inlay_hints.enabled
+                    && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_))
+                {
+                    return None;
+                }
+            }
+            None => return None,
+        }
 
-                if !editor.registered_buffers.contains_key(&query.buffer_id)
-                    && let Some(project) = editor.project.as_ref() {
-                        project.update(cx, |project, cx| {
-                            editor.registered_buffers.insert(
-                                query.buffer_id,
-                                project.register_buffer_with_language_servers(&buffer, cx),
-                            );
-                        })
-                    }
+        Some(invalidate_cache)
+    }
 
-                editor
-                    .semantics_provider
-                    .as_ref()?
-                    .inlay_hints(buffer, fetch_range.clone(), cx)
-            })
-            .ok()
-            .flatten();
+    pub(crate) fn visible_inlay_hints(&self, cx: &Context<Editor>) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
+            .cloned()
+            .collect()
+    }
 
-        let cached_excerpt_hints = editor.read_with(cx, |editor, _| {
-            editor
-                .inlay_hint_cache
-                .hints
-                .get(&query.excerpt_id)
-                .cloned()
-        })?;
-
-        let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::<Vec<_>>())?;
-        let new_hints = match inlay_hints_fetch_task {
-            Some(fetch_task) => {
-                log::debug!(
-                    "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
-                    query_reason = query.reason,
-                );
-                log::trace!(
-                    "Currently visible hints: {visible_hints:?}, cached hints present: {}",
-                    cached_excerpt_hints.is_some(),
-                );
-                fetch_task.await.context("inlay hint fetch task")?
-            }
-            None => return Ok(()),
+    pub fn update_inlay_link_and_hover_points(
+        &mut self,
+        snapshot: &EditorSnapshot,
+        point_for_position: PointForPosition,
+        secondary_held: bool,
+        shift_held: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else {
+            return;
         };
-        drop(lsp_request_guard);
-        log::debug!(
-            "Fetched {} hints for range {fetch_range_to_log:?}",
-            new_hints.len()
-        );
-        log::trace!("Fetched hints: {new_hints:?}");
-
-        let background_task_buffer_snapshot = buffer_snapshot.clone();
-        let background_fetch_range = fetch_range.clone();
-        let new_update = cx.background_spawn(async move {
-            calculate_hint_updates(
-                query.excerpt_id,
-                invalidate,
-                background_fetch_range,
-                new_hints,
-                &background_task_buffer_snapshot,
-                cached_excerpt_hints,
-                &visible_hints,
+        let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
+            Some(
+                snapshot
+                    .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left),
             )
-        })
-            .await;
-        if let Some(new_update) = new_update {
-            log::debug!(
-                "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
-                new_update.remove_from_visible.len(),
-                new_update.remove_from_cache.len(),
-                new_update.add_to_cache.len()
+        } else {
+            None
+        };
+        let mut go_to_definition_updated = false;
+        let mut hover_updated = false;
+        if let Some(hovered_offset) = hovered_offset {
+            let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+            let previous_valid_anchor = buffer_snapshot.anchor_at(
+                point_for_position.previous_valid.to_point(snapshot),
+                Bias::Left,
             );
-            log::trace!("New update: {new_update:?}");
-            editor
-                .update(cx, |editor,  cx| {
-                    apply_hint_update(
-                        editor,
-                        new_update,
-                        query,
-                        invalidate,
-                        buffer_snapshot,
-                        multi_buffer_snapshot,
-                        cx,
-                    );
+            let next_valid_anchor = buffer_snapshot.anchor_at(
+                point_for_position.next_valid.to_point(snapshot),
+                Bias::Right,
+            );
+            if let Some(hovered_hint) = self
+                .visible_inlay_hints(cx)
+                .into_iter()
+                .skip_while(|hint| {
+                    hint.position
+                        .cmp(&previous_valid_anchor, &buffer_snapshot)
+                        .is_lt()
                 })
-                .ok();
-        }
-        anyhow::Ok(())
-    })
-}
-
-fn calculate_hint_updates(
-    excerpt_id: ExcerptId,
-    invalidate: bool,
-    fetch_range: Range<language::Anchor>,
-    new_excerpt_hints: Vec<InlayHint>,
-    buffer_snapshot: &BufferSnapshot,
-    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
-    visible_hints: &[Inlay],
-) -> Option<ExcerptHintsUpdate> {
-    let mut add_to_cache = Vec::<InlayHint>::new();
-    let mut excerpt_hints_to_persist = HashMap::default();
-    for new_hint in new_excerpt_hints {
-        if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
-            continue;
-        }
-        let missing_from_cache = match &cached_excerpt_hints {
-            Some(cached_excerpt_hints) => {
-                let cached_excerpt_hints = cached_excerpt_hints.read();
-                match cached_excerpt_hints
-                    .ordered_hints
-                    .binary_search_by(|probe| {
-                        cached_excerpt_hints.hints_by_id[probe]
-                            .position
-                            .cmp(&new_hint.position, buffer_snapshot)
-                    }) {
-                    Ok(ix) => {
-                        let mut missing_from_cache = true;
-                        for id in &cached_excerpt_hints.ordered_hints[ix..] {
-                            let cached_hint = &cached_excerpt_hints.hints_by_id[id];
-                            if new_hint
-                                .position
-                                .cmp(&cached_hint.position, buffer_snapshot)
-                                .is_gt()
-                            {
-                                break;
+                .take_while(|hint| {
+                    hint.position
+                        .cmp(&next_valid_anchor, &buffer_snapshot)
+                        .is_le()
+                })
+                .max_by_key(|hint| hint.id)
+            {
+                if let Some(ResolvedHint::Resolved(cached_hint)) =
+                    hovered_hint.position.buffer_id.and_then(|buffer_id| {
+                        lsp_store.update(cx, |lsp_store, cx| {
+                            lsp_store.resolved_hint(buffer_id, hovered_hint.id, cx)
+                        })
+                    })
+                {
+                    match cached_hint.resolve_state {
+                        ResolveState::Resolved => {
+                            let mut extra_shift_left = 0;
+                            let mut extra_shift_right = 0;
+                            if cached_hint.padding_left {
+                                extra_shift_left += 1;
+                                extra_shift_right += 1;
                             }
-                            if cached_hint == &new_hint {
-                                excerpt_hints_to_persist.insert(*id, cached_hint.kind);
-                                missing_from_cache = false;
+                            if cached_hint.padding_right {
+                                extra_shift_right += 1;
                             }
+                            match cached_hint.label {
+                                InlayHintLabel::String(_) => {
+                                    if let Some(tooltip) = cached_hint.tooltip {
+                                        hover_popover::hover_at_inlay(
+                                            self,
+                                            InlayHover {
+                                                tooltip: match tooltip {
+                                                    InlayHintTooltip::String(text) => HoverBlock {
+                                                        text,
+                                                        kind: HoverBlockKind::PlainText,
+                                                    },
+                                                    InlayHintTooltip::MarkupContent(content) => {
+                                                        HoverBlock {
+                                                            text: content.value,
+                                                            kind: content.kind,
+                                                        }
+                                                    }
+                                                },
+                                                range: InlayHighlight {
+                                                    inlay: hovered_hint.id,
+                                                    inlay_position: hovered_hint.position,
+                                                    range: extra_shift_left
+                                                        ..hovered_hint.text().len()
+                                                            + extra_shift_right,
+                                                },
+                                            },
+                                            window,
+                                            cx,
+                                        );
+                                        hover_updated = true;
+                                    }
+                                }
+                                InlayHintLabel::LabelParts(label_parts) => {
+                                    let hint_start =
+                                        snapshot.anchor_to_inlay_offset(hovered_hint.position);
+                                    if let Some((hovered_hint_part, part_range)) =
+                                        hover_popover::find_hovered_hint_part(
+                                            label_parts,
+                                            hint_start,
+                                            hovered_offset,
+                                        )
+                                    {
+                                        let highlight_start =
+                                            (part_range.start - hint_start).0 + extra_shift_left;
+                                        let highlight_end =
+                                            (part_range.end - hint_start).0 + extra_shift_right;
+                                        let highlight = InlayHighlight {
+                                            inlay: hovered_hint.id,
+                                            inlay_position: hovered_hint.position,
+                                            range: highlight_start..highlight_end,
+                                        };
+                                        if let Some(tooltip) = hovered_hint_part.tooltip {
+                                            hover_popover::hover_at_inlay(
+                                                self,
+                                                InlayHover {
+                                                    tooltip: match tooltip {
+                                                        InlayHintLabelPartTooltip::String(text) => {
+                                                            HoverBlock {
+                                                                text,
+                                                                kind: HoverBlockKind::PlainText,
+                                                            }
+                                                        }
+                                                        InlayHintLabelPartTooltip::MarkupContent(
+                                                            content,
+                                                        ) => HoverBlock {
+                                                            text: content.value,
+                                                            kind: content.kind,
+                                                        },
+                                                    },
+                                                    range: highlight.clone(),
+                                                },
+                                                window,
+                                                cx,
+                                            );
+                                            hover_updated = true;
+                                        }
+                                        if let Some((language_server_id, location)) =
+                                            hovered_hint_part.location
+                                            && secondary_held
+                                            && !self.has_pending_nonempty_selection()
+                                        {
+                                            go_to_definition_updated = true;
+                                            show_link_definition(
+                                                shift_held,
+                                                self,
+                                                TriggerPoint::InlayHint(
+                                                    highlight,
+                                                    location,
+                                                    language_server_id,
+                                                ),
+                                                snapshot,
+                                                window,
+                                                cx,
+                                            );
+                                        }
+                                    }
+                                }
+                            };
                         }
-                        missing_from_cache
+                        ResolveState::CanResolve(_, _) => debug_panic!(
+                            "Expected resolved_hint retrieval to return a resolved hint"
+                        ),
+                        ResolveState::Resolving => {}
                     }
-                    Err(_) => true,
                 }
             }
-            None => true,
-        };
-        if missing_from_cache {
-            add_to_cache.push(new_hint);
+        }
+
+        if !go_to_definition_updated {
+            self.hide_hovered_link(cx)
+        }
+        if !hover_updated {
+            hover_popover::hover_at(self, None, window, cx);
         }
     }
 
-    let mut remove_from_visible = HashSet::default();
-    let mut remove_from_cache = HashSet::default();
-    if invalidate {
-        remove_from_visible.extend(
-            visible_hints
-                .iter()
-                .filter(|hint| hint.position.excerpt_id == excerpt_id)
-                .map(|inlay_hint| inlay_hint.id)
-                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
-        );
+    fn inlay_hints_for_buffer(
+        &mut self,
+        invalidate_cache: InvalidationStrategy,
+        ignore_previous_fetches: bool,
+        buffer_excerpts: VisibleExcerpts,
+        cx: &mut Context<Self>,
+    ) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
+        let semantics_provider = self.semantics_provider()?;
+        let inlay_hints = self.inlay_hints.as_mut()?;
+        let buffer_id = buffer_excerpts.buffer.read(cx).remote_id();
+
+        let new_hint_tasks = semantics_provider
+            .inlay_hints(
+                invalidate_cache,
+                buffer_excerpts.buffer,
+                buffer_excerpts.ranges,
+                inlay_hints
+                    .hint_chunk_fetched
+                    .get(&buffer_id)
+                    .filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate())
+                    .cloned(),
+                cx,
+            )
+            .unwrap_or_default();
+
+        let (known_version, known_chunks) =
+            inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
+        if buffer_excerpts.buffer_version.changed_since(known_version) {
+            known_chunks.clear();
+            *known_version = buffer_excerpts.buffer_version;
+        }
 
-        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
-            let cached_excerpt_hints = cached_excerpt_hints.read();
-            remove_from_cache.extend(
-                cached_excerpt_hints
-                    .ordered_hints
+        let mut hint_tasks = Vec::new();
+        for (row_range, new_hints_task) in new_hint_tasks {
+            let inserted = known_chunks.insert(row_range.clone());
+            if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() {
+                hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
+            }
+        }
+
+        Some(hint_tasks)
+    }
+
+    fn apply_fetched_hints(
+        &mut self,
+        buffer_id: BufferId,
+        query_version: Global,
+        invalidate_cache: InvalidationStrategy,
+        new_hints: Vec<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>,
+        all_affected_buffers: Arc<Mutex<HashSet<BufferId>>>,
+        cx: &mut Context<Self>,
+    ) {
+        let visible_inlay_hint_ids = self
+            .visible_inlay_hints(cx)
+            .iter()
+            .filter(|inlay| inlay.position.buffer_id == Some(buffer_id))
+            .map(|inlay| inlay.id)
+            .collect::<Vec<_>>();
+        let Some(inlay_hints) = &mut self.inlay_hints else {
+            return;
+        };
+
+        let mut hints_to_remove = Vec::new();
+        let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+
+        // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there,
+        // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache.
+        // So, if we hover such hints, no resolve will happen.
+        //
+        // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed.
+        // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored
+        // from the cache.
+        if invalidate_cache.should_invalidate() {
+            hints_to_remove.extend(visible_inlay_hint_ids);
+        }
+
+        let excerpts = self.buffer.read(cx).excerpt_ids();
+        let hints_to_insert = new_hints
+            .into_iter()
+            .filter_map(|(chunk_range, hints_result)| match hints_result {
+                Ok(new_hints) => Some(new_hints),
+                Err(e) => {
+                    log::error!(
+                        "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
+                    );
+                    if let Some((for_version, chunks_fetched)) =
+                        inlay_hints.hint_chunk_fetched.get_mut(&buffer_id)
+                    {
+                        if for_version == &query_version {
+                            chunks_fetched.remove(&chunk_range);
+                        }
+                    }
+                    None
+                }
+            })
+            .flat_map(|hints| hints.into_values())
+            .flatten()
+            .filter_map(|(hint_id, lsp_hint)| {
+                if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
+                    && inlay_hints
+                        .added_hints
+                        .insert(hint_id, lsp_hint.kind)
+                        .is_none()
+                {
+                    let position = excerpts.iter().find_map(|excerpt_id| {
+                        multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, lsp_hint.position)
+                    })?;
+                    return Some(Inlay::hint(hint_id, position, &lsp_hint));
+                }
+                None
+            })
+            .collect::<Vec<_>>();
+
+        // We need to invalidate excerpts all buffers with the same language, do that once only, after first new data chunk is inserted.
+        let all_other_affected_buffers = all_affected_buffers
+            .lock()
+            .drain()
+            .filter(|id| buffer_id != *id)
+            .collect::<HashSet<_>>();
+        if !all_other_affected_buffers.is_empty() {
+            hints_to_remove.extend(
+                self.visible_inlay_hints(cx)
                     .iter()
-                    .filter(|cached_inlay_id| {
-                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+                    .filter(|inlay| {
+                        inlay
+                            .position
+                            .buffer_id
+                            .is_none_or(|buffer_id| all_other_affected_buffers.contains(&buffer_id))
                     })
-                    .copied(),
+                    .map(|inlay| inlay.id),
             );
-            remove_from_visible.extend(remove_from_cache.iter().cloned());
         }
-    }
 
-    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
-        None
-    } else {
-        Some(ExcerptHintsUpdate {
-            excerpt_id,
-            remove_from_visible,
-            remove_from_cache,
-            add_to_cache,
-        })
+        self.splice_inlays(&hints_to_remove, hints_to_insert, cx);
     }
 }
 
-fn contains_position(
-    range: &Range<language::Anchor>,
-    position: language::Anchor,
-    buffer_snapshot: &BufferSnapshot,
-) -> bool {
-    range.start.cmp(&position, buffer_snapshot).is_le()
-        && range.end.cmp(&position, buffer_snapshot).is_ge()
+#[derive(Debug)]
+struct VisibleExcerpts {
+    excerpts: Vec<ExcerptId>,
+    ranges: Vec<Range<text::Anchor>>,
+    buffer_version: Global,
+    buffer: Entity<language::Buffer>,
 }
 
-fn apply_hint_update(
-    editor: &mut Editor,
-    new_update: ExcerptHintsUpdate,
-    query: ExcerptQuery,
-    invalidate: bool,
-    buffer_snapshot: BufferSnapshot,
-    multi_buffer_snapshot: MultiBufferSnapshot,
-    cx: &mut Context<Editor>,
-) {
-    let cached_excerpt_hints = editor
-        .inlay_hint_cache
-        .hints
-        .entry(new_update.excerpt_id)
-        .or_insert_with(|| {
-            Arc::new(RwLock::new(CachedExcerptHints {
-                version: query.cache_version,
-                buffer_version: buffer_snapshot.version().clone(),
-                buffer_id: query.buffer_id,
-                ordered_hints: Vec::new(),
-                hints_by_id: HashMap::default(),
-            }))
-        });
-    let mut cached_excerpt_hints = cached_excerpt_hints.write();
-    match query.cache_version.cmp(&cached_excerpt_hints.version) {
-        cmp::Ordering::Less => return,
-        cmp::Ordering::Greater | cmp::Ordering::Equal => {
-            cached_excerpt_hints.version = query.cache_version;
+fn spawn_editor_hints_refresh(
+    buffer_id: BufferId,
+    invalidate_cache: InvalidationStrategy,
+    ignore_previous_fetches: bool,
+    debounce: Option<Duration>,
+    buffer_excerpts: VisibleExcerpts,
+    all_affected_buffers: Arc<Mutex<HashSet<BufferId>>>,
+    cx: &mut Context<'_, Editor>,
+) -> Task<()> {
+    cx.spawn(async move |editor, cx| {
+        if let Some(debounce) = debounce {
+            cx.background_executor().timer(debounce).await;
         }
-    }
 
-    let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
-    cached_excerpt_hints
-        .ordered_hints
-        .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
-    cached_excerpt_hints
-        .hints_by_id
-        .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
-    let mut splice = InlaySplice::default();
-    splice.to_remove.extend(new_update.remove_from_visible);
-    for new_hint in new_update.add_to_cache {
-        let insert_position = match cached_excerpt_hints
-            .ordered_hints
-            .binary_search_by(|probe| {
-                cached_excerpt_hints.hints_by_id[probe]
-                    .position
-                    .cmp(&new_hint.position, &buffer_snapshot)
-            }) {
-            Ok(i) => {
-                // When a hint is added to the same position where existing ones are present,
-                // do not deduplicate it: we split hint queries into non-overlapping ranges
-                // and each hint batch returned by the server should already contain unique hints.
-                i + cached_excerpt_hints.ordered_hints[i..].len() + 1
-            }
-            Err(i) => i,
+        let query_version = buffer_excerpts.buffer_version.clone();
+        let Some(hint_tasks) = editor
+            .update(cx, |editor, cx| {
+                editor.inlay_hints_for_buffer(
+                    invalidate_cache,
+                    ignore_previous_fetches,
+                    buffer_excerpts,
+                    cx,
+                )
+            })
+            .ok()
+        else {
+            return;
         };
-
-        let new_inlay_id = post_inc(&mut editor.next_inlay_id);
-        if editor
-            .inlay_hint_cache
-            .allowed_hint_kinds
-            .contains(&new_hint.kind)
-            && let Some(new_hint_position) =
-                multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
-        {
-            splice
-                .to_insert
-                .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
-        }
-        let new_id = InlayId::Hint(new_inlay_id);
-        cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
-        if cached_excerpt_hints.ordered_hints.len() <= insert_position {
-            cached_excerpt_hints.ordered_hints.push(new_id);
-        } else {
-            cached_excerpt_hints
-                .ordered_hints
-                .insert(insert_position, new_id);
-        }
-
-        cached_inlays_changed = true;
-    }
-    cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
-    drop(cached_excerpt_hints);
-
-    if invalidate {
-        let mut outdated_excerpt_caches = HashSet::default();
-        for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
-            let excerpt_hints = excerpt_hints.read();
-            if excerpt_hints.buffer_id == query.buffer_id
-                && excerpt_id != &query.excerpt_id
-                && buffer_snapshot
-                    .version()
-                    .changed_since(&excerpt_hints.buffer_version)
-            {
-                outdated_excerpt_caches.insert(*excerpt_id);
-                splice
-                    .to_remove
-                    .extend(excerpt_hints.ordered_hints.iter().copied());
-            }
+        let hint_tasks = hint_tasks.unwrap_or_default();
+        if hint_tasks.is_empty() {
+            return;
         }
-        cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
+        let new_hints = join_all(hint_tasks).await;
         editor
-            .inlay_hint_cache
-            .hints
-            .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
-    }
-
-    let InlaySplice {
-        to_remove,
-        to_insert,
-    } = splice;
-    let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
-    if cached_inlays_changed || displayed_inlays_changed {
-        editor.inlay_hint_cache.version += 1;
-    }
-    if displayed_inlays_changed {
-        editor.splice_inlays(&to_remove, to_insert, cx)
-    }
+            .update(cx, |editor, cx| {
+                editor.apply_fetched_hints(
+                    buffer_id,
+                    query_version,
+                    invalidate_cache,
+                    new_hints,
+                    all_affected_buffers,
+                    cx,
+                );
+            })
+            .ok();
+    })
 }
 
 #[cfg(test)]
 pub mod tests {
-    use crate::SelectionEffects;
     use crate::editor_tests::update_test_language_settings;
+    use crate::inlays::inlay_hints::InlayHintRefreshReason;
     use crate::scroll::ScrollAmount;
-    use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
-    use futures::StreamExt;
+    use crate::{Editor, SelectionEffects};
+    use crate::{ExcerptRange, scroll::Autoscroll};
+    use collections::HashSet;
+    use futures::{StreamExt, future};
     use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle};
     use itertools::Itertools as _;
+    use language::language_settings::InlayHintKind;
     use language::{Capability, FakeLspAdapter};
     use language::{Language, LanguageConfig, LanguageMatcher};
+    use languages::rust_lang;
     use lsp::FakeLanguageServer;
+    use multi_buffer::MultiBuffer;
     use parking_lot::Mutex;
+    use pretty_assertions::assert_eq;
     use project::{FakeFs, Project};
     use serde_json::json;
     use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore};
+    use std::ops::Range;
+    use std::sync::Arc;
     use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
-    use text::Point;
+    use std::time::Duration;
+    use text::{OffsetRangeExt, Point};
+    use ui::App;
     use util::path;
-
-    use super::*;
+    use util::paths::natural_sort;
 
     #[gpui::test]
     async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {

crates/editor/src/lsp_colors.rs 🔗

@@ -6,15 +6,15 @@ use gpui::{Hsla, Rgba, Task};
 use itertools::Itertools;
 use language::point_from_lsp;
 use multi_buffer::Anchor;
-use project::DocumentColor;
+use project::{DocumentColor, InlayId};
 use settings::Settings as _;
 use text::{Bias, BufferId, OffsetRangeExt as _};
 use ui::{App, Context, Window};
 use util::post_inc;
 
 use crate::{
-    DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId,
-    InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode,
+    DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT,
+    InlaySplice, RangeToAnchorExt, editor_settings::DocumentColorsRenderMode, inlays::Inlay,
 };
 
 #[derive(Debug)]
@@ -164,7 +164,7 @@ impl Editor {
         }
 
         let visible_buffers = self
-            .visible_excerpts(None, cx)
+            .visible_excerpts(cx)
             .into_values()
             .map(|(buffer, ..)| buffer)
             .filter(|editor_buffer| {
@@ -400,8 +400,7 @@ impl Editor {
                     }
 
                     if colors.render_mode == DocumentColorsRenderMode::Inlay
-                        && (!colors_splice.to_insert.is_empty()
-                            || !colors_splice.to_remove.is_empty())
+                        && !colors_splice.is_empty()
                     {
                         editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx);
                         updated = true;

crates/editor/src/movement.rs 🔗

@@ -872,7 +872,7 @@ mod tests {
     use super::*;
     use crate::{
         Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer,
-        display_map::Inlay,
+        inlays::Inlay,
         test::{editor_test_context::EditorTestContext, marked_display_snapshot},
     };
     use gpui::{AppContext as _, font, px};

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -1,14 +1,14 @@
 use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
 use buffer_diff::BufferDiff;
-use collections::HashSet;
+use collections::{HashMap, HashSet};
 use futures::{channel::mpsc, future::join_all};
 use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
-use language::{Buffer, BufferEvent, Capability};
+use language::{Buffer, BufferEvent, BufferRow, Capability};
 use multi_buffer::{ExcerptRange, MultiBuffer};
-use project::Project;
+use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints};
 use smol::stream::StreamExt;
 use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
-use text::ToOffset;
+use text::{BufferId, ToOffset};
 use ui::{ButtonLike, KeyBinding, prelude::*};
 use workspace::{
     Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -436,14 +436,34 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         self.0.hover(&buffer, position, cx)
     }
 
+    fn applicable_inlay_chunks(
+        &self,
+        buffer_id: BufferId,
+        ranges: &[Range<text::Anchor>],
+        cx: &App,
+    ) -> Vec<Range<BufferRow>> {
+        self.0.applicable_inlay_chunks(buffer_id, ranges, cx)
+    }
+
+    fn invalidate_inlay_hints(&self, for_buffers: &HashSet<BufferId>, cx: &mut App) {
+        self.0.invalidate_inlay_hints(for_buffers, cx);
+    }
+
     fn inlay_hints(
         &self,
+        invalidate: InvalidationStrategy,
         buffer: Entity<Buffer>,
-        range: Range<text::Anchor>,
+        ranges: Vec<Range<text::Anchor>>,
+        known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
         cx: &mut App,
-    ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
-        let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
-        self.0.inlay_hints(buffer, range, cx)
+    ) -> Option<HashMap<Range<BufferRow>, Task<anyhow::Result<CacheInlayHints>>>> {
+        let positions = ranges
+            .iter()
+            .flat_map(|range| [range.start, range.end])
+            .collect::<Vec<_>>();
+        let buffer = self.to_base(&buffer, &positions, cx)?;
+        self.0
+            .inlay_hints(invalidate, buffer, ranges, known_chunks, cx)
     }
 
     fn inline_values(
@@ -455,17 +475,6 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
         None
     }
 
-    fn resolve_inlay_hint(
-        &self,
-        hint: project::InlayHint,
-        buffer: Entity<Buffer>,
-        server_id: lsp::LanguageServerId,
-        cx: &mut App,
-    ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
-        let buffer = self.to_base(&buffer, &[], cx)?;
-        self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
-    }
-
     fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
         if let Some(buffer) = self.to_base(buffer, &[], cx) {
             self.0.supports_inlay_hints(&buffer, cx)

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -6,6 +6,7 @@ use std::{
 };
 
 use anyhow::Result;
+use language::rust_lang;
 use serde_json::json;
 
 use crate::{Editor, ToPoint};
@@ -32,55 +33,6 @@ pub struct EditorLspTestContext {
     pub buffer_lsp_url: lsp::Uri,
 }
 
-pub(crate) fn rust_lang() -> Arc<Language> {
-    let language = Language::new(
-        LanguageConfig {
-            name: "Rust".into(),
-            matcher: LanguageMatcher {
-                path_suffixes: vec!["rs".to_string()],
-                ..Default::default()
-            },
-            line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::LANGUAGE.into()),
-    )
-    .with_queries(LanguageQueries {
-        indents: Some(Cow::from(indoc! {r#"
-            [
-                ((where_clause) _ @end)
-                (field_expression)
-                (call_expression)
-                (assignment_expression)
-                (let_declaration)
-                (let_chain)
-                (await_expression)
-            ] @indent
-
-            (_ "[" "]" @end) @indent
-            (_ "<" ">" @end) @indent
-            (_ "{" "}" @end) @indent
-            (_ "(" ")" @end) @indent"#})),
-        brackets: Some(Cow::from(indoc! {r#"
-            ("(" @open ")" @close)
-            ("[" @open "]" @close)
-            ("{" @open "}" @close)
-            ("<" @open ">" @close)
-            ("\"" @open "\"" @close)
-            (closure_parameters "|" @open "|" @close)"#})),
-        text_objects: Some(Cow::from(indoc! {r#"
-            (function_item
-                body: (_
-                    "{"
-                    (_)* @function.inside
-                    "}" )) @function.around
-        "#})),
-        ..Default::default()
-    })
-    .expect("Could not parse queries");
-    Arc::new(language)
-}
-
 #[cfg(test)]
 pub(crate) fn git_commit_lang() -> Arc<Language> {
     Arc::new(Language::new(

crates/language/src/language.rs 🔗

@@ -2600,6 +2600,65 @@ pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
     start..end
 }
 
+#[doc(hidden)]
+#[cfg(any(test, feature = "test-support"))]
+pub fn rust_lang() -> Arc<Language> {
+    use std::borrow::Cow;
+
+    let language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::LANGUAGE.into()),
+    )
+    .with_queries(LanguageQueries {
+        indents: Some(Cow::from(
+            r#"
+[
+    ((where_clause) _ @end)
+    (field_expression)
+    (call_expression)
+    (assignment_expression)
+    (let_declaration)
+    (let_chain)
+    (await_expression)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent"#,
+        )),
+        brackets: Some(Cow::from(
+            r#"
+("(" @open ")" @close)
+("[" @open "]" @close)
+("{" @open "}" @close)
+("<" @open ">" @close)
+("\"" @open "\"" @close)
+(closure_parameters "|" @open "|" @close)"#,
+        )),
+        text_objects: Some(Cow::from(
+            r#"
+(function_item
+    body: (_
+        "{"
+        (_)* @function.inside
+        "}" )) @function.around
+        "#,
+        )),
+        ..LanguageQueries::default()
+    })
+    .expect("Could not parse queries");
+    Arc::new(language)
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/project/src/lsp_command.rs 🔗

@@ -234,7 +234,7 @@ pub(crate) struct OnTypeFormatting {
     pub push_to_history: bool,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub(crate) struct InlayHints {
     pub range: Range<Anchor>,
 }

crates/project/src/lsp_store.rs 🔗

@@ -16,16 +16,20 @@ pub mod lsp_ext_command;
 pub mod rust_analyzer_ext;
 pub mod vue_language_server_ext;
 
+mod inlay_hint_cache;
+
+use self::inlay_hint_cache::BufferInlayHints;
 use crate::{
     CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse,
-    CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction,
-    LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath,
+    CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, InlayId, LocationLink,
+    LspAction, LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath,
     ProjectTransaction, PulledDiagnostics, ResolveState, Symbol,
     buffer_store::{BufferStore, BufferStoreEvent},
     environment::ProjectEnvironment,
     lsp_command::{self, *},
     lsp_store::{
         self,
+        inlay_hint_cache::BufferChunk,
         log_store::{GlobalLogStore, LanguageServerKind},
     },
     manifest_tree::{
@@ -57,7 +61,7 @@ use gpui::{
 use http_client::HttpClient;
 use itertools::Itertools as _;
 use language::{
-    Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
+    Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
     DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName,
     LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate,
     ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain,
@@ -85,7 +89,7 @@ use parking_lot::Mutex;
 use postage::{mpsc, sink::Sink, stream::Stream, watch};
 use rand::prelude::*;
 use rpc::{
-    AnyProtoClient,
+    AnyProtoClient, ErrorCode, ErrorExt as _,
     proto::{LspRequestId, LspRequestMessage as _},
 };
 use serde::Serialize;
@@ -106,11 +110,14 @@ use std::{
     path::{self, Path, PathBuf},
     pin::pin,
     rc::Rc,
-    sync::Arc,
+    sync::{
+        Arc,
+        atomic::{self, AtomicUsize},
+    },
     time::{Duration, Instant},
 };
 use sum_tree::Dimensions;
-use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _};
+use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, Point, ToPoint as _};
 
 use util::{
     ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
@@ -121,6 +128,7 @@ use util::{
 
 pub use fs::*;
 pub use language::Location;
+pub use lsp_store::inlay_hint_cache::{CacheInlayHints, InvalidationStrategy};
 #[cfg(any(test, feature = "test-support"))]
 pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
 pub use worktree::{
@@ -565,8 +573,7 @@ impl LocalLspStore {
     }
 
     fn setup_lsp_messages(
-        this: WeakEntity<LspStore>,
-
+        lsp_store: WeakEntity<LspStore>,
         language_server: &LanguageServer,
         delegate: Arc<dyn LspAdapterDelegate>,
         adapter: Arc<CachedLspAdapter>,
@@ -576,7 +583,7 @@ impl LocalLspStore {
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |mut params, cx| {
                     let adapter = adapter.clone();
                     if let Some(this) = this.upgrade() {
@@ -620,8 +627,7 @@ impl LocalLspStore {
             .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                 let adapter = adapter.adapter.clone();
                 let delegate = delegate.clone();
-                let this = this.clone();
-
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let adapter = adapter.clone();
                     let delegate = delegate.clone();
@@ -666,7 +672,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::WorkspaceFoldersRequest, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |_, cx| {
                     let this = this.clone();
                     let cx = cx.clone();
@@ -694,7 +700,7 @@ impl LocalLspStore {
         // to these requests when initializing.
         language_server
             .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let this = this.clone();
                     let mut cx = cx.clone();
@@ -715,7 +721,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::RegisterCapability, _, _>({
-                let lsp_store = this.clone();
+                let lsp_store = lsp_store.clone();
                 move |params, cx| {
                     let lsp_store = lsp_store.clone();
                     let mut cx = cx.clone();
@@ -744,7 +750,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::UnregisterCapability, _, _>({
-                let lsp_store = this.clone();
+                let lsp_store = lsp_store.clone();
                 move |params, cx| {
                     let lsp_store = lsp_store.clone();
                     let mut cx = cx.clone();
@@ -773,7 +779,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let mut cx = cx.clone();
                     let this = this.clone();
@@ -792,18 +798,22 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
-                let this = this.clone();
+                let lsp_store = lsp_store.clone();
                 move |(), cx| {
-                    let this = this.clone();
+                    let this = lsp_store.clone();
                     let mut cx = cx.clone();
                     async move {
-                        this.update(&mut cx, |this, cx| {
-                            cx.emit(LspStoreEvent::RefreshInlayHints);
-                            this.downstream_client.as_ref().map(|(client, project_id)| {
-                                client.send(proto::RefreshInlayHints {
-                                    project_id: *project_id,
+                        this.update(&mut cx, |lsp_store, cx| {
+                            cx.emit(LspStoreEvent::RefreshInlayHints(server_id));
+                            lsp_store
+                                .downstream_client
+                                .as_ref()
+                                .map(|(client, project_id)| {
+                                    client.send(proto::RefreshInlayHints {
+                                        project_id: *project_id,
+                                        server_id: server_id.to_proto(),
+                                    })
                                 })
-                            })
                         })?
                         .transpose()?;
                         Ok(())
@@ -814,7 +824,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::CodeLensRefresh, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |(), cx| {
                     let this = this.clone();
                     let mut cx = cx.clone();
@@ -836,7 +846,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::WorkspaceDiagnosticRefresh, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |(), cx| {
                     let this = this.clone();
                     let mut cx = cx.clone();
@@ -862,7 +872,7 @@ impl LocalLspStore {
 
         language_server
             .on_request::<lsp::request::ShowMessageRequest, _, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 let name = name.to_string();
                 move |params, cx| {
                     let this = this.clone();
@@ -900,7 +910,7 @@ impl LocalLspStore {
             .detach();
         language_server
             .on_notification::<lsp::notification::ShowMessage, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 let name = name.to_string();
                 move |params, cx| {
                     let this = this.clone();
@@ -932,7 +942,7 @@ impl LocalLspStore {
 
         language_server
             .on_notification::<lsp::notification::Progress, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     if let Some(this) = this.upgrade() {
                         this.update(cx, |this, cx| {
@@ -951,7 +961,7 @@ impl LocalLspStore {
 
         language_server
             .on_notification::<lsp::notification::LogMessage, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     if let Some(this) = this.upgrade() {
                         this.update(cx, |_, cx| {
@@ -969,7 +979,7 @@ impl LocalLspStore {
 
         language_server
             .on_notification::<lsp::notification::LogTrace, _>({
-                let this = this.clone();
+                let this = lsp_store.clone();
                 move |params, cx| {
                     let mut cx = cx.clone();
                     if let Some(this) = this.upgrade() {
@@ -988,10 +998,10 @@ impl LocalLspStore {
             })
             .detach();
 
-        vue_language_server_ext::register_requests(this.clone(), language_server);
-        json_language_server_ext::register_requests(this.clone(), language_server);
-        rust_analyzer_ext::register_notifications(this.clone(), language_server);
-        clangd_ext::register_notifications(this, language_server, adapter);
+        vue_language_server_ext::register_requests(lsp_store.clone(), language_server);
+        json_language_server_ext::register_requests(lsp_store.clone(), language_server);
+        rust_analyzer_ext::register_notifications(lsp_store.clone(), language_server);
+        clangd_ext::register_notifications(lsp_store, language_server, adapter);
     }
 
     fn shutdown_language_servers_on_quit(
@@ -3498,9 +3508,55 @@ pub struct LspStore {
     diagnostic_summaries:
         HashMap<WorktreeId, HashMap<Arc<RelPath>, HashMap<LanguageServerId, DiagnosticSummary>>>,
     pub lsp_server_capabilities: HashMap<LanguageServerId, lsp::ServerCapabilities>,
-    lsp_document_colors: HashMap<BufferId, DocumentColorData>,
-    lsp_code_lens: HashMap<BufferId, CodeLensData>,
-    running_lsp_requests: HashMap<TypeId, (Global, HashMap<LspRequestId, Task<()>>)>,
+    lsp_data: HashMap<BufferId, BufferLspData>,
+    next_hint_id: Arc<AtomicUsize>,
+}
+
+#[derive(Debug)]
+pub struct BufferLspData {
+    buffer_version: Global,
+    document_colors: Option<DocumentColorData>,
+    code_lens: Option<CodeLensData>,
+    inlay_hints: BufferInlayHints,
+    lsp_requests: HashMap<LspKey, HashMap<LspRequestId, Task<()>>>,
+    chunk_lsp_requests: HashMap<LspKey, HashMap<BufferChunk, LspRequestId>>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+struct LspKey {
+    request_type: TypeId,
+    server_queried: Option<LanguageServerId>,
+}
+
+impl BufferLspData {
+    fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
+        Self {
+            buffer_version: buffer.read(cx).version(),
+            document_colors: None,
+            code_lens: None,
+            inlay_hints: BufferInlayHints::new(buffer, cx),
+            lsp_requests: HashMap::default(),
+            chunk_lsp_requests: HashMap::default(),
+        }
+    }
+
+    fn remove_server_data(&mut self, for_server: LanguageServerId) {
+        if let Some(document_colors) = &mut self.document_colors {
+            document_colors.colors.remove(&for_server);
+            document_colors.cache_version += 1;
+        }
+
+        if let Some(code_lens) = &mut self.code_lens {
+            code_lens.lens.remove(&for_server);
+        }
+
+        self.inlay_hints.remove_server_data(for_server);
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn inlay_hints(&self) -> &BufferInlayHints {
+        &self.inlay_hints
+    }
 }
 
 #[derive(Debug, Default, Clone)]
@@ -3514,7 +3570,6 @@ type CodeLensTask = Shared<Task<std::result::Result<Option<Vec<CodeAction>>, Arc
 
 #[derive(Debug, Default)]
 struct DocumentColorData {
-    colors_for_version: Global,
     colors: HashMap<LanguageServerId, HashSet<DocumentColor>>,
     cache_version: usize,
     colors_update: Option<(Global, DocumentColorTask)>,
@@ -3522,7 +3577,6 @@ struct DocumentColorData {
 
 #[derive(Debug, Default)]
 struct CodeLensData {
-    lens_for_version: Global,
     lens: HashMap<LanguageServerId, Vec<CodeAction>>,
     update: Option<(Global, CodeLensTask)>,
 }
@@ -3543,7 +3597,7 @@ pub enum LspStoreEvent {
         new_language: Option<Arc<Language>>,
     },
     Notification(String),
-    RefreshInlayHints,
+    RefreshInlayHints(LanguageServerId),
     RefreshCodeLens,
     DiagnosticsUpdated {
         server_id: LanguageServerId,
@@ -3615,7 +3669,6 @@ impl LspStore {
         client.add_entity_request_handler(Self::handle_apply_code_action_kind);
         client.add_entity_request_handler(Self::handle_resolve_completion_documentation);
         client.add_entity_request_handler(Self::handle_apply_code_action);
-        client.add_entity_request_handler(Self::handle_inlay_hints);
         client.add_entity_request_handler(Self::handle_get_project_symbols);
         client.add_entity_request_handler(Self::handle_resolve_inlay_hint);
         client.add_entity_request_handler(Self::handle_get_color_presentation);
@@ -3765,9 +3818,8 @@ impl LspStore {
             nonce: StdRng::from_os_rng().random(),
             diagnostic_summaries: HashMap::default(),
             lsp_server_capabilities: HashMap::default(),
-            lsp_document_colors: HashMap::default(),
-            lsp_code_lens: HashMap::default(),
-            running_lsp_requests: HashMap::default(),
+            lsp_data: HashMap::default(),
+            next_hint_id: Arc::default(),
             active_entry: None,
             _maintain_workspace_config,
             _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
@@ -3826,9 +3878,8 @@ impl LspStore {
             nonce: StdRng::from_os_rng().random(),
             diagnostic_summaries: HashMap::default(),
             lsp_server_capabilities: HashMap::default(),
-            lsp_document_colors: HashMap::default(),
-            lsp_code_lens: HashMap::default(),
-            running_lsp_requests: HashMap::default(),
+            next_hint_id: Arc::default(),
+            lsp_data: HashMap::default(),
             active_entry: None,
 
             _maintain_workspace_config,
@@ -4025,8 +4076,7 @@ impl LspStore {
                         *refcount
                     };
                     if refcount == 0 {
-                        lsp_store.lsp_document_colors.remove(&buffer_id);
-                        lsp_store.lsp_code_lens.remove(&buffer_id);
+                        lsp_store.lsp_data.remove(&buffer_id);
                         let local = lsp_store.as_local_mut().unwrap();
                         local.registered_buffers.remove(&buffer_id);
                         local.buffers_opened_in_servers.remove(&buffer_id);
@@ -4293,7 +4343,7 @@ impl LspStore {
         &self,
         buffer: &Entity<Buffer>,
         request: &R,
-        cx: &Context<Self>,
+        cx: &App,
     ) -> bool
     where
         R: LspCommand,
@@ -4314,7 +4364,7 @@ impl LspStore {
         &self,
         buffer: &Entity<Buffer>,
         check: F,
-        cx: &Context<Self>,
+        cx: &App,
     ) -> bool
     where
         F: Fn(&lsp::ServerCapabilities) -> bool,
@@ -4800,7 +4850,65 @@ impl LspStore {
         }
     }
 
-    pub fn resolve_inlay_hint(
+    pub fn resolved_hint(
+        &mut self,
+        buffer_id: BufferId,
+        id: InlayId,
+        cx: &mut Context<Self>,
+    ) -> Option<ResolvedHint> {
+        let buffer = self.buffer_store.read(cx).get(buffer_id)?;
+
+        let lsp_data = self.lsp_data.get_mut(&buffer_id)?;
+        let buffer_lsp_hints = &mut lsp_data.inlay_hints;
+        let hint = buffer_lsp_hints.hint_for_id(id)?.clone();
+        let (server_id, resolve_data) = match &hint.resolve_state {
+            ResolveState::Resolved => return Some(ResolvedHint::Resolved(hint)),
+            ResolveState::Resolving => {
+                return Some(ResolvedHint::Resolving(
+                    buffer_lsp_hints.hint_resolves.get(&id)?.clone(),
+                ));
+            }
+            ResolveState::CanResolve(server_id, resolve_data) => (*server_id, resolve_data.clone()),
+        };
+
+        let resolve_task = self.resolve_inlay_hint(hint, buffer, server_id, cx);
+        let buffer_lsp_hints = &mut self.lsp_data.get_mut(&buffer_id)?.inlay_hints;
+        let previous_task = buffer_lsp_hints.hint_resolves.insert(
+            id,
+            cx.spawn(async move |lsp_store, cx| {
+                let resolved_hint = resolve_task.await;
+                lsp_store
+                    .update(cx, |lsp_store, _| {
+                        if let Some(old_inlay_hint) = lsp_store
+                            .lsp_data
+                            .get_mut(&buffer_id)
+                            .and_then(|buffer_lsp_data| buffer_lsp_data.inlay_hints.hint_for_id(id))
+                        {
+                            match resolved_hint {
+                                Ok(resolved_hint) => {
+                                    *old_inlay_hint = resolved_hint;
+                                }
+                                Err(e) => {
+                                    old_inlay_hint.resolve_state =
+                                        ResolveState::CanResolve(server_id, resolve_data);
+                                    log::error!("Inlay hint resolve failed: {e:#}");
+                                }
+                            }
+                        }
+                    })
+                    .ok();
+            })
+            .shared(),
+        );
+        debug_assert!(
+            previous_task.is_none(),
+            "Did not change hint's resolve state after spawning its resolve"
+        );
+        buffer_lsp_hints.hint_for_id(id)?.resolve_state = ResolveState::Resolving;
+        None
+    }
+
+    fn resolve_inlay_hint(
         &self,
         mut hint: InlayHint,
         buffer: Entity<Buffer>,
@@ -5149,6 +5257,7 @@ impl LspStore {
             }
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -5214,6 +5323,7 @@ impl LspStore {
             }
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -5279,6 +5389,7 @@ impl LspStore {
             }
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -5344,6 +5455,7 @@ impl LspStore {
             }
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -5410,6 +5522,7 @@ impl LspStore {
 
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -5477,6 +5590,7 @@ impl LspStore {
             }
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -5538,32 +5652,38 @@ impl LspStore {
     ) -> CodeLensTask {
         let version_queried_for = buffer.read(cx).version();
         let buffer_id = buffer.read(cx).remote_id();
+        let existing_servers = self.as_local().map(|local| {
+            local
+                .buffers_opened_in_servers
+                .get(&buffer_id)
+                .cloned()
+                .unwrap_or_default()
+        });
 
-        if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id)
-            && !version_queried_for.changed_since(&cached_data.lens_for_version)
-        {
-            let has_different_servers = self.as_local().is_some_and(|local| {
-                local
-                    .buffers_opened_in_servers
-                    .get(&buffer_id)
-                    .cloned()
-                    .unwrap_or_default()
-                    != cached_data.lens.keys().copied().collect()
-            });
-            if !has_different_servers {
-                return Task::ready(Ok(Some(
-                    cached_data.lens.values().flatten().cloned().collect(),
-                )))
-                .shared();
+        if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
+            if let Some(cached_lens) = &lsp_data.code_lens {
+                if !version_queried_for.changed_since(&lsp_data.buffer_version) {
+                    let has_different_servers = existing_servers.is_some_and(|existing_servers| {
+                        existing_servers != cached_lens.lens.keys().copied().collect()
+                    });
+                    if !has_different_servers {
+                        return Task::ready(Ok(Some(
+                            cached_lens.lens.values().flatten().cloned().collect(),
+                        )))
+                        .shared();
+                    }
+                } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() {
+                    if !version_queried_for.changed_since(updating_for) {
+                        return running_update.clone();
+                    }
+                }
             }
         }
 
-        let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default();
-        if let Some((updating_for, running_update)) = &lsp_data.update
-            && !version_queried_for.changed_since(updating_for)
-        {
-            return running_update.clone();
-        }
+        let lens_lsp_data = self
+            .latest_lsp_data(buffer, cx)
+            .code_lens
+            .get_or_insert_default();
         let buffer = buffer.clone();
         let query_version_queried_for = version_queried_for.clone();
         let new_task = cx
@@ -5582,7 +5702,13 @@ impl LspStore {
                     Err(e) => {
                         lsp_store
                             .update(cx, |lsp_store, _| {
-                                lsp_store.lsp_code_lens.entry(buffer_id).or_default().update = None;
+                                if let Some(lens_lsp_data) = lsp_store
+                                    .lsp_data
+                                    .get_mut(&buffer_id)
+                                    .and_then(|lsp_data| lsp_data.code_lens.as_mut())
+                                {
+                                    lens_lsp_data.update = None;
+                                }
                             })
                             .ok();
                         return Err(e);
@@ -5591,25 +5717,26 @@ impl LspStore {
 
                 lsp_store
                     .update(cx, |lsp_store, _| {
-                        let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default();
+                        let lsp_data = lsp_store.current_lsp_data(buffer_id)?;
+                        let code_lens = lsp_data.code_lens.as_mut()?;
                         if let Some(fetched_lens) = fetched_lens {
-                            if lsp_data.lens_for_version == query_version_queried_for {
-                                lsp_data.lens.extend(fetched_lens);
+                            if lsp_data.buffer_version == query_version_queried_for {
+                                code_lens.lens.extend(fetched_lens);
                             } else if !lsp_data
-                                .lens_for_version
+                                .buffer_version
                                 .changed_since(&query_version_queried_for)
                             {
-                                lsp_data.lens_for_version = query_version_queried_for;
-                                lsp_data.lens = fetched_lens;
+                                lsp_data.buffer_version = query_version_queried_for;
+                                code_lens.lens = fetched_lens;
                             }
                         }
-                        lsp_data.update = None;
-                        Some(lsp_data.lens.values().flatten().cloned().collect())
+                        code_lens.update = None;
+                        Some(code_lens.lens.values().flatten().cloned().collect())
                     })
                     .map_err(Arc::new)
             })
             .shared();
-        lsp_data.update = Some((version_queried_for, new_task.clone()));
+        lens_lsp_data.update = Some((version_queried_for, new_task.clone()));
         new_task
     }
 
@@ -5625,6 +5752,7 @@ impl LspStore {
             }
             let request_task = upstream_client.request_lsp(
                 project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(project_id, buffer.read(cx)),
@@ -6327,6 +6455,7 @@ impl LspStore {
             }
             let request_task = client.request_lsp(
                 upstream_project_id,
+                None,
                 LSP_REQUEST_TIMEOUT,
                 cx.background_executor().clone(),
                 request.to_proto(upstream_project_id, buffer.read(cx)),
@@ -6369,58 +6498,308 @@ impl LspStore {
         }
     }
 
+    pub fn applicable_inlay_chunks(
+        &self,
+        buffer_id: BufferId,
+        ranges: &[Range<text::Anchor>],
+    ) -> Vec<Range<BufferRow>> {
+        self.lsp_data
+            .get(&buffer_id)
+            .map(|data| {
+                data.inlay_hints
+                    .applicable_chunks(ranges)
+                    .map(|chunk| chunk.start..chunk.end)
+                    .collect()
+            })
+            .unwrap_or_default()
+    }
+
+    pub fn invalidate_inlay_hints<'a>(
+        &'a mut self,
+        for_buffers: impl IntoIterator<Item = &'a BufferId> + 'a,
+    ) {
+        for buffer_id in for_buffers {
+            if let Some(lsp_data) = self.lsp_data.get_mut(buffer_id) {
+                lsp_data.inlay_hints.clear();
+            }
+        }
+    }
+
     pub fn inlay_hints(
         &mut self,
+        invalidate: InvalidationStrategy,
         buffer: Entity<Buffer>,
-        range: Range<Anchor>,
+        ranges: Vec<Range<text::Anchor>>,
+        known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
         cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
-        let range_start = range.start;
-        let range_end = range.end;
-        let buffer_id = buffer.read(cx).remote_id().into();
-        let request = InlayHints { range };
+    ) -> HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>> {
+        let buffer_snapshot = buffer.read(cx).snapshot();
+        let for_server = if let InvalidationStrategy::RefreshRequested(server_id) = invalidate {
+            Some(server_id)
+        } else {
+            None
+        };
+        let invalidate_cache = invalidate.should_invalidate();
+        let next_hint_id = self.next_hint_id.clone();
+        let lsp_data = self.latest_lsp_data(&buffer, cx);
+        let existing_inlay_hints = &mut lsp_data.inlay_hints;
+        let known_chunks = known_chunks
+            .filter(|(known_version, _)| !lsp_data.buffer_version.changed_since(known_version))
+            .map(|(_, known_chunks)| known_chunks)
+            .unwrap_or_default();
 
-        if let Some((client, project_id)) = self.upstream_client() {
-            if !self.is_capable_for_proto_request(&buffer, &request, cx) {
-                return Task::ready(Ok(Vec::new()));
+        let mut hint_fetch_tasks = Vec::new();
+        let mut cached_inlay_hints = HashMap::default();
+        let mut ranges_to_query = Vec::new();
+        let applicable_chunks = existing_inlay_hints
+            .applicable_chunks(ranges.as_slice())
+            .filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end)))
+            .collect::<Vec<_>>();
+        if applicable_chunks.is_empty() {
+            return HashMap::default();
+        }
+
+        let last_chunk_number = applicable_chunks.len() - 1;
+
+        for (i, row_chunk) in applicable_chunks.into_iter().enumerate() {
+            match (
+                existing_inlay_hints
+                    .cached_hints(&row_chunk)
+                    .filter(|_| !invalidate_cache)
+                    .cloned(),
+                existing_inlay_hints
+                    .fetched_hints(&row_chunk)
+                    .as_ref()
+                    .filter(|_| !invalidate_cache)
+                    .cloned(),
+            ) {
+                (None, None) => {
+                    let end = if last_chunk_number == i {
+                        Point::new(row_chunk.end, buffer_snapshot.line_len(row_chunk.end))
+                    } else {
+                        Point::new(row_chunk.end, 0)
+                    };
+                    ranges_to_query.push((
+                        row_chunk,
+                        buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0))
+                            ..buffer_snapshot.anchor_after(end),
+                    ));
+                }
+                (None, Some(fetched_hints)) => {
+                    hint_fetch_tasks.push((row_chunk, fetched_hints.clone()))
+                }
+                (Some(cached_hints), None) => {
+                    for (server_id, cached_hints) in cached_hints {
+                        if for_server.is_none_or(|for_server| for_server == server_id) {
+                            cached_inlay_hints
+                                .entry(row_chunk.start..row_chunk.end)
+                                .or_insert_with(HashMap::default)
+                                .entry(server_id)
+                                .or_insert_with(Vec::new)
+                                .extend(cached_hints);
+                        }
+                    }
+                }
+                (Some(cached_hints), Some(fetched_hints)) => {
+                    hint_fetch_tasks.push((row_chunk, fetched_hints.clone()));
+                    for (server_id, cached_hints) in cached_hints {
+                        if for_server.is_none_or(|for_server| for_server == server_id) {
+                            cached_inlay_hints
+                                .entry(row_chunk.start..row_chunk.end)
+                                .or_insert_with(HashMap::default)
+                                .entry(server_id)
+                                .or_insert_with(Vec::new)
+                                .extend(cached_hints);
+                        }
+                    }
+                }
             }
-            let proto_request = proto::InlayHints {
-                project_id,
-                buffer_id,
-                start: Some(serialize_anchor(&range_start)),
-                end: Some(serialize_anchor(&range_end)),
-                version: serialize_version(&buffer.read(cx).version()),
-            };
-            cx.spawn(async move |project, cx| {
-                let response = client
-                    .request(proto_request)
-                    .await
-                    .context("inlay hints proto request")?;
-                LspCommand::response_from_proto(
-                    request,
-                    response,
-                    project.upgrade().context("No project")?,
-                    buffer.clone(),
-                    cx.clone(),
+        }
+
+        let cached_chunk_data = cached_inlay_hints
+            .into_iter()
+            .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
+            .collect();
+        if hint_fetch_tasks.is_empty() && ranges_to_query.is_empty() {
+            cached_chunk_data
+        } else {
+            if invalidate_cache {
+                lsp_data.inlay_hints.clear();
+            }
+
+            for (chunk, range_to_query) in ranges_to_query {
+                let next_hint_id = next_hint_id.clone();
+                let buffer = buffer.clone();
+                let new_inlay_hints = cx
+                    .spawn(async move |lsp_store, cx| {
+                        let new_fetch_task = lsp_store.update(cx, |lsp_store, cx| {
+                            lsp_store.fetch_inlay_hints(for_server, &buffer, range_to_query, cx)
+                        })?;
+                        new_fetch_task
+                            .await
+                            .and_then(|new_hints_by_server| {
+                                lsp_store.update(cx, |lsp_store, cx| {
+                                    let lsp_data = lsp_store.latest_lsp_data(&buffer, cx);
+                                    let update_cache = !lsp_data
+                                        .buffer_version
+                                        .changed_since(&buffer.read(cx).version());
+                                    new_hints_by_server
+                                        .into_iter()
+                                        .map(|(server_id, new_hints)| {
+                                            let new_hints = new_hints
+                                                .into_iter()
+                                                .map(|new_hint| {
+                                                    (
+                                                        InlayId::Hint(next_hint_id.fetch_add(
+                                                            1,
+                                                            atomic::Ordering::AcqRel,
+                                                        )),
+                                                        new_hint,
+                                                    )
+                                                })
+                                                .collect::<Vec<_>>();
+                                            if update_cache {
+                                                lsp_data.inlay_hints.insert_new_hints(
+                                                    chunk,
+                                                    server_id,
+                                                    new_hints.clone(),
+                                                );
+                                            }
+                                            (server_id, new_hints)
+                                        })
+                                        .collect()
+                                })
+                            })
+                            .map_err(Arc::new)
+                    })
+                    .shared();
+
+                let fetch_task = lsp_data.inlay_hints.fetched_hints(&chunk);
+                *fetch_task = Some(new_inlay_hints.clone());
+                hint_fetch_tasks.push((chunk, new_inlay_hints));
+            }
+
+            let mut combined_data = cached_chunk_data;
+            combined_data.extend(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| {
+                (
+                    chunk.start..chunk.end,
+                    cx.spawn(async move |_, _| {
+                        hints_fetch.await.map_err(|e| {
+                            if e.error_code() != ErrorCode::Internal {
+                                anyhow!(e.error_code())
+                            } else {
+                                anyhow!("{e:#}")
+                            }
+                        })
+                    }),
                 )
-                .await
-                .context("inlay hints proto response conversion")
+            }));
+            combined_data
+        }
+    }
+
+    fn fetch_inlay_hints(
+        &mut self,
+        for_server: Option<LanguageServerId>,
+        buffer: &Entity<Buffer>,
+        range: Range<Anchor>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<HashMap<LanguageServerId, Vec<InlayHint>>>> {
+        let request = InlayHints {
+            range: range.clone(),
+        };
+        if let Some((upstream_client, project_id)) = self.upstream_client() {
+            if !self.is_capable_for_proto_request(buffer, &request, cx) {
+                return Task::ready(Ok(HashMap::default()));
+            }
+            let request_task = upstream_client.request_lsp(
+                project_id,
+                for_server.map(|id| id.to_proto()),
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
+            let buffer = buffer.clone();
+            cx.spawn(async move |weak_lsp_store, cx| {
+                let Some(lsp_store) = weak_lsp_store.upgrade() else {
+                    return Ok(HashMap::default());
+                };
+                let Some(responses) = request_task.await? else {
+                    return Ok(HashMap::default());
+                };
+
+                let inlay_hints = join_all(responses.payload.into_iter().map(|response| {
+                    let lsp_store = lsp_store.clone();
+                    let buffer = buffer.clone();
+                    let cx = cx.clone();
+                    let request = request.clone();
+                    async move {
+                        (
+                            LanguageServerId::from_proto(response.server_id),
+                            request
+                                .response_from_proto(response.response, lsp_store, buffer, cx)
+                                .await,
+                        )
+                    }
+                }))
+                .await;
+
+                let mut has_errors = false;
+                let inlay_hints = inlay_hints
+                    .into_iter()
+                    .filter_map(|(server_id, inlay_hints)| match inlay_hints {
+                        Ok(inlay_hints) => Some((server_id, inlay_hints)),
+                        Err(e) => {
+                            has_errors = true;
+                            log::error!("{e:#}");
+                            None
+                        }
+                    })
+                    .collect::<HashMap<_, _>>();
+                anyhow::ensure!(
+                    !has_errors || !inlay_hints.is_empty(),
+                    "Failed to fetch inlay hints"
+                );
+                Ok(inlay_hints)
             })
         } else {
-            let lsp_request_task = self.request_lsp(
-                buffer.clone(),
-                LanguageServerToQuery::FirstCapable,
-                request,
-                cx,
-            );
-            cx.spawn(async move |_, cx| {
-                buffer
-                    .update(cx, |buffer, _| {
-                        buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp])
-                    })?
+            let inlay_hints_task = match for_server {
+                Some(server_id) => {
+                    let server_task = self.request_lsp(
+                        buffer.clone(),
+                        LanguageServerToQuery::Other(server_id),
+                        request,
+                        cx,
+                    );
+                    cx.background_spawn(async move {
+                        let mut responses = Vec::new();
+                        match server_task.await {
+                            Ok(response) => responses.push((server_id, response)),
+                            Err(e) => log::error!(
+                                "Error handling response for inlay hints request: {e:#}"
+                            ),
+                        }
+                        responses
+                    })
+                }
+                None => self.request_multiple_lsp_locally(buffer, None::<usize>, request, cx),
+            };
+            let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+            cx.background_spawn(async move {
+                Ok(inlay_hints_task
                     .await
-                    .context("waiting for inlay hint request range edits")?;
-                lsp_request_task.await.context("inlay hints LSP request")
+                    .into_iter()
+                    .map(|(server_id, mut new_hints)| {
+                        new_hints.retain(|hint| {
+                            hint.position.is_valid(&buffer_snapshot)
+                                && range.start.is_valid(&buffer_snapshot)
+                                && range.end.is_valid(&buffer_snapshot)
+                                && hint.position.cmp(&range.start, &buffer_snapshot).is_ge()
+                                && hint.position.cmp(&range.end, &buffer_snapshot).is_le()
+                        });
+                        (server_id, new_hints)
+                    })
+                    .collect())
             })
         }
     }
@@ -6531,39 +6910,55 @@ impl LspStore {
         let version_queried_for = buffer.read(cx).version();
         let buffer_id = buffer.read(cx).remote_id();
 
-        if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id)
-            && !version_queried_for.changed_since(&cached_data.colors_for_version)
-        {
-            let has_different_servers = self.as_local().is_some_and(|local| {
-                local
-                    .buffers_opened_in_servers
-                    .get(&buffer_id)
-                    .cloned()
-                    .unwrap_or_default()
-                    != cached_data.colors.keys().copied().collect()
-            });
-            if !has_different_servers {
-                if Some(cached_data.cache_version) == known_cache_version {
-                    return None;
-                } else {
-                    return Some(
-                        Task::ready(Ok(DocumentColors {
-                            colors: cached_data.colors.values().flatten().cloned().collect(),
-                            cache_version: Some(cached_data.cache_version),
-                        }))
-                        .shared(),
-                    );
+        let current_language_servers = self.as_local().map(|local| {
+            local
+                .buffers_opened_in_servers
+                .get(&buffer_id)
+                .cloned()
+                .unwrap_or_default()
+        });
+
+        if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
+            if let Some(cached_colors) = &lsp_data.document_colors {
+                if !version_queried_for.changed_since(&lsp_data.buffer_version) {
+                    let has_different_servers =
+                        current_language_servers.is_some_and(|current_language_servers| {
+                            current_language_servers
+                                != cached_colors.colors.keys().copied().collect()
+                        });
+                    if !has_different_servers {
+                        let cache_version = cached_colors.cache_version;
+                        if Some(cache_version) == known_cache_version {
+                            return None;
+                        } else {
+                            return Some(
+                                Task::ready(Ok(DocumentColors {
+                                    colors: cached_colors
+                                        .colors
+                                        .values()
+                                        .flatten()
+                                        .cloned()
+                                        .collect(),
+                                    cache_version: Some(cache_version),
+                                }))
+                                .shared(),
+                            );
+                        }
+                    }
                 }
             }
         }
 
-        let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default();
-        if let Some((updating_for, running_update)) = &lsp_data.colors_update
+        let color_lsp_data = self
+            .latest_lsp_data(&buffer, cx)
+            .document_colors
+            .get_or_insert_default();
+        if let Some((updating_for, running_update)) = &color_lsp_data.colors_update
             && !version_queried_for.changed_since(updating_for)
         {
             return Some(running_update.clone());
         }
-        let query_version_queried_for = version_queried_for.clone();
+        let buffer_version_queried_for = version_queried_for.clone();
         let new_task = cx
             .spawn(async move |lsp_store, cx| {
                 cx.background_executor()

crates/project/src/lsp_store/inlay_hint_cache.rs 🔗

@@ -0,0 +1,221 @@
+use std::{collections::hash_map, ops::Range, sync::Arc};
+
+use collections::HashMap;
+use futures::future::Shared;
+use gpui::{App, Entity, Task};
+use language::{Buffer, BufferRow, BufferSnapshot};
+use lsp::LanguageServerId;
+use text::OffsetRangeExt;
+
+use crate::{InlayHint, InlayId};
+
+pub type CacheInlayHints = HashMap<LanguageServerId, Vec<(InlayId, InlayHint)>>;
+pub type CacheInlayHintsTask = Shared<Task<Result<CacheInlayHints, Arc<anyhow::Error>>>>;
+
+/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts.
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+    /// Language servers reset hints via <a href="https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_inlayHint_refresh">request</a>.
+    /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation.
+    ///
+    /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise.
+    RefreshRequested(LanguageServerId),
+    /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place.
+    /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence.
+    BufferEdited,
+    /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position.
+    /// No invalidation should be done at all, all new hints are added to the cache.
+    ///
+    /// A special case is the editor toggles and settings change:
+    /// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints.
+    /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen.
+    None,
+}
+
+impl InvalidationStrategy {
+    pub fn should_invalidate(&self) -> bool {
+        matches!(
+            self,
+            InvalidationStrategy::RefreshRequested(_) | InvalidationStrategy::BufferEdited
+        )
+    }
+}
+
+pub struct BufferInlayHints {
+    snapshot: BufferSnapshot,
+    buffer_chunks: Vec<BufferChunk>,
+    hints_by_chunks: Vec<Option<CacheInlayHints>>,
+    fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
+    hints_by_id: HashMap<InlayId, HintForId>,
+    pub(super) hint_resolves: HashMap<InlayId, Shared<Task<()>>>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct HintForId {
+    chunk_id: usize,
+    server_id: LanguageServerId,
+    position: usize,
+}
+
+/// An range of rows, exclusive as [`lsp::Range`] and
+/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
+/// denote.
+///
+/// Represents an area in a text editor, adjacent to other ones.
+/// Together, chunks form entire document at a particular version [`clock::Global`].
+/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
+/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct BufferChunk {
+    id: usize,
+    pub start: BufferRow,
+    pub end: BufferRow,
+}
+
+impl std::fmt::Debug for BufferInlayHints {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("BufferInlayHints")
+            .field("buffer_chunks", &self.buffer_chunks)
+            .field("hints_by_chunks", &self.hints_by_chunks)
+            .field("fetches_by_chunks", &self.fetches_by_chunks)
+            .field("hints_by_id", &self.hints_by_id)
+            .finish_non_exhaustive()
+    }
+}
+
+const MAX_ROWS_IN_A_CHUNK: u32 = 50;
+
+impl BufferInlayHints {
+    pub fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
+        let buffer = buffer.read(cx);
+        let snapshot = buffer.snapshot();
+        let buffer_point_range = (0..buffer.len()).to_point(&snapshot);
+        let last_row = buffer_point_range.end.row;
+        let buffer_chunks = (buffer_point_range.start.row..=last_row)
+            .step_by(MAX_ROWS_IN_A_CHUNK as usize)
+            .enumerate()
+            .map(|(id, chunk_start)| BufferChunk {
+                id,
+                start: chunk_start,
+                end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row),
+            })
+            .collect::<Vec<_>>();
+
+        Self {
+            hints_by_chunks: vec![None; buffer_chunks.len()],
+            fetches_by_chunks: vec![None; buffer_chunks.len()],
+            hints_by_id: HashMap::default(),
+            hint_resolves: HashMap::default(),
+            snapshot,
+            buffer_chunks,
+        }
+    }
+
+    pub fn applicable_chunks(
+        &self,
+        ranges: &[Range<text::Anchor>],
+    ) -> impl Iterator<Item = BufferChunk> {
+        let row_ranges = ranges
+            .iter()
+            .map(|range| range.to_point(&self.snapshot))
+            .map(|point_range| point_range.start.row..=point_range.end.row)
+            .collect::<Vec<_>>();
+        self.buffer_chunks
+            .iter()
+            .filter(move |chunk| -> bool {
+                // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
+                // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
+                let chunk_range = chunk.start..=chunk.end;
+                row_ranges.iter().any(|row_range| {
+                    chunk_range.contains(&row_range.start())
+                        || chunk_range.contains(&row_range.end())
+                })
+            })
+            .copied()
+    }
+
+    pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> {
+        self.hints_by_chunks[chunk.id].as_ref()
+    }
+
+    pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option<CacheInlayHintsTask> {
+        &mut self.fetches_by_chunks[chunk.id]
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn all_cached_hints(&self) -> Vec<InlayHint> {
+        self.hints_by_chunks
+            .iter()
+            .filter_map(|hints| hints.as_ref())
+            .flat_map(|hints| hints.values().cloned())
+            .flatten()
+            .map(|(_, hint)| hint)
+            .collect()
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn all_fetched_hints(&self) -> Vec<CacheInlayHintsTask> {
+        self.fetches_by_chunks
+            .iter()
+            .filter_map(|fetches| fetches.clone())
+            .collect()
+    }
+
+    pub fn remove_server_data(&mut self, for_server: LanguageServerId) {
+        for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() {
+            if let Some(hints) = hints {
+                if hints.remove(&for_server).is_some() {
+                    self.fetches_by_chunks[chunk_index] = None;
+                }
+            }
+        }
+    }
+
+    pub fn clear(&mut self) {
+        self.hints_by_chunks = vec![None; self.buffer_chunks.len()];
+        self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
+        self.hints_by_id.clear();
+        self.hint_resolves.clear();
+    }
+
+    pub fn insert_new_hints(
+        &mut self,
+        chunk: BufferChunk,
+        server_id: LanguageServerId,
+        new_hints: Vec<(InlayId, InlayHint)>,
+    ) {
+        let existing_hints = self.hints_by_chunks[chunk.id]
+            .get_or_insert_default()
+            .entry(server_id)
+            .or_insert_with(Vec::new);
+        let existing_count = existing_hints.len();
+        existing_hints.extend(new_hints.into_iter().enumerate().filter_map(
+            |(i, (id, new_hint))| {
+                let new_hint_for_id = HintForId {
+                    chunk_id: chunk.id,
+                    server_id,
+                    position: existing_count + i,
+                };
+                if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) {
+                    vacant_entry.insert(new_hint_for_id);
+                    Some((id, new_hint))
+                } else {
+                    None
+                }
+            },
+        ));
+        *self.fetched_hints(&chunk) = None;
+    }
+
+    pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> {
+        let hint_for_id = self.hints_by_id.get(&id)?;
+        let (hint_id, hint) = self
+            .hints_by_chunks
+            .get_mut(hint_for_id.chunk_id)?
+            .as_mut()?
+            .get_mut(&hint_for_id.server_id)?
+            .get_mut(hint_for_id.position)?;
+        debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}");
+        Some(hint)
+    }
+}

crates/project/src/project.rs 🔗

@@ -145,9 +145,9 @@ pub use task_inventory::{
 
 pub use buffer_store::ProjectTransaction;
 pub use lsp_store::{
-    DiagnosticSummary, LanguageServerLogType, LanguageServerProgress, LanguageServerPromptRequest,
-    LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
-    SERVER_PROGRESS_THROTTLE_TIMEOUT,
+    DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress,
+    LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore,
+    LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT,
 };
 pub use toolchain_store::{ToolchainStore, Toolchains};
 const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
@@ -338,7 +338,7 @@ pub enum Event {
     HostReshared,
     Reshared,
     Rejoined,
-    RefreshInlayHints,
+    RefreshInlayHints(LanguageServerId),
     RefreshCodeLens,
     RevealInProjectPanel(ProjectEntryId),
     SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
@@ -402,6 +402,26 @@ pub enum PrepareRenameResponse {
     InvalidPosition,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+    EditPrediction(usize),
+    DebuggerValue(usize),
+    // LSP
+    Hint(usize),
+    Color(usize),
+}
+
+impl InlayId {
+    pub fn id(&self) -> usize {
+        match self {
+            Self::EditPrediction(id) => *id,
+            Self::DebuggerValue(id) => *id,
+            Self::Hint(id) => *id,
+            Self::Color(id) => *id,
+        }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct InlayHint {
     pub position: language::Anchor,
@@ -3058,7 +3078,9 @@ impl Project {
                     return;
                 };
             }
-            LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
+            LspStoreEvent::RefreshInlayHints(server_id) => {
+                cx.emit(Event::RefreshInlayHints(*server_id))
+            }
             LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
             LspStoreEvent::LanguageServerPrompt(prompt) => {
                 cx.emit(Event::LanguageServerPrompt(prompt.clone()))
@@ -3978,31 +4000,6 @@ impl Project {
         })
     }
 
-    pub fn inlay_hints<T: ToOffset>(
-        &mut self,
-        buffer_handle: Entity<Buffer>,
-        range: Range<T>,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<Vec<InlayHint>>> {
-        let buffer = buffer_handle.read(cx);
-        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.inlay_hints(buffer_handle, range, cx)
-        })
-    }
-
-    pub fn resolve_inlay_hint(
-        &self,
-        hint: InlayHint,
-        buffer_handle: Entity<Buffer>,
-        server_id: LanguageServerId,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<InlayHint>> {
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.resolve_inlay_hint(hint, buffer_handle, server_id, cx)
-        })
-    }
-
     pub fn search(&mut self, query: SearchQuery, cx: &mut Context<Self>) -> Receiver<SearchResult> {
         let (result_tx, result_rx) = smol::channel::unbounded();
 
@@ -5262,6 +5259,7 @@ impl Project {
             })
     }
 
+    #[cfg(any(test, feature = "test-support"))]
     pub fn has_language_servers_for(&self, buffer: &Buffer, cx: &mut App) -> bool {
         self.lsp_store.update(cx, |this, cx| {
             this.language_servers_for_local_buffer(buffer, cx)

crates/project/src/project_tests.rs 🔗

@@ -1815,7 +1815,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
     fake_server
         .start_progress(format!("{}/0", progress_token))
         .await;
-    assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::RefreshInlayHints(fake_server.server.server_id())
+    );
     assert_eq!(
         events.next().await.unwrap(),
         Event::DiskBasedDiagnosticsStarted {
@@ -1954,7 +1957,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
             Some(worktree_id)
         )
     );
-    assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
+    assert_eq!(
+        events.next().await.unwrap(),
+        Event::RefreshInlayHints(fake_server.server.server_id())
+    );
     fake_server.start_progress(progress_token).await;
     assert_eq!(
         events.next().await.unwrap(),

crates/proto/proto/lsp.proto 🔗

@@ -465,6 +465,7 @@ message ResolveInlayHintResponse {
 
 message RefreshInlayHints {
     uint64 project_id = 1;
+    uint64 server_id = 2;
 }
 
 message CodeLens {
@@ -781,6 +782,7 @@ message TextEdit {
 message LspQuery {
     uint64 project_id = 1;
     uint64 lsp_request_id = 2;
+    optional uint64 server_id = 15;
     oneof request {
         GetReferences get_references = 3;
         GetDocumentColor get_document_color = 4;
@@ -793,6 +795,7 @@ message LspQuery {
         GetDeclaration get_declaration = 11;
         GetTypeDefinition get_type_definition = 12;
         GetImplementation get_implementation = 13;
+        InlayHints inlay_hints = 14;
     }
 }
 
@@ -815,6 +818,7 @@ message LspResponse {
         GetTypeDefinitionResponse get_type_definition_response = 10;
         GetImplementationResponse get_implementation_response = 11;
         GetReferencesResponse get_references_response = 12;
+        InlayHintsResponse inlay_hints_response = 13;
     }
     uint64 server_id = 7;
 }

crates/proto/src/proto.rs 🔗

@@ -517,6 +517,7 @@ lsp_messages!(
     (GetDeclaration, GetDeclarationResponse, true),
     (GetTypeDefinition, GetTypeDefinitionResponse, true),
     (GetImplementation, GetImplementationResponse, true),
+    (InlayHints, InlayHintsResponse, false),
 );
 
 entity_messages!(
@@ -847,6 +848,7 @@ impl LspQuery {
             Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false),
             Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false),
             Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false),
+            Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false),
             None => ("<unknown>", true),
         }
     }

crates/rpc/src/proto_client.rs 🔗

@@ -226,6 +226,7 @@ impl AnyProtoClient {
     pub fn request_lsp<T>(
         &self,
         project_id: u64,
+        server_id: Option<u64>,
         timeout: Duration,
         executor: BackgroundExecutor,
         request: T,
@@ -247,6 +248,7 @@ impl AnyProtoClient {
 
         let query = proto::LspQuery {
             project_id,
+            server_id,
             lsp_request_id: new_id.0,
             request: Some(request.to_proto_query()),
         };
@@ -361,6 +363,9 @@ impl AnyProtoClient {
                             Response::GetImplementationResponse(response) => {
                                 to_any_envelope(&envelope, response)
                             }
+                            Response::InlayHintsResponse(response) => {
+                                to_any_envelope(&envelope, response)
+                            }
                         };
                         Some(proto::ProtoLspResponse {
                             server_id,

crates/search/Cargo.toml 🔗

@@ -47,5 +47,7 @@ zed_actions.workspace = true
 client = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
+language = { workspace = true, features = ["test-support"] }
+lsp.workspace = true
 unindent.workspace = true
 workspace = { workspace = true, features = ["test-support"] }

crates/search/src/project_search.rs 🔗

@@ -2357,9 +2357,10 @@ pub mod tests {
     use super::*;
     use editor::{DisplayPoint, display_map::DisplayRow};
     use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
+    use language::{FakeLspAdapter, rust_lang};
     use project::FakeFs;
     use serde_json::json;
-    use settings::SettingsStore;
+    use settings::{InlayHintSettingsContent, SettingsStore};
     use util::{path, paths::PathStyle, rel_path::rel_path};
     use util_macros::perf;
     use workspace::DeploySearch;
@@ -4226,6 +4227,101 @@ pub mod tests {
             .unwrap();
     }
 
+    #[perf]
+    #[gpui::test]
+    async fn test_search_with_inlays(cx: &mut TestAppContext) {
+        init_test(cx);
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |store, cx| {
+                store.update_user_settings(cx, |settings| {
+                    settings.project.all_languages.defaults.inlay_hints =
+                        Some(InlayHintSettingsContent {
+                            enabled: Some(true),
+                            ..InlayHintSettingsContent::default()
+                        })
+                });
+            });
+        });
+
+        let fs = FakeFs::new(cx.background_executor.clone());
+        fs.insert_tree(
+            path!("/dir"),
+            // `\n` , a trailing line on the end, is important for the test case
+            json!({
+                "main.rs": "fn main() { let a = 2; }\n",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        let language = rust_lang();
+        language_registry.add(language);
+        let mut fake_servers = language_registry.register_fake_lsp(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..lsp::ServerCapabilities::default()
+                },
+                initializer: Some(Box::new(|fake_server| {
+                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
+                        move |_, _| async move {
+                            Ok(Some(vec![lsp::InlayHint {
+                                position: lsp::Position::new(0, 17),
+                                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,
+                            }]))
+                        },
+                    );
+                })),
+                ..FakeLspAdapter::default()
+            },
+        );
+
+        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+        let workspace = window.root(cx).unwrap();
+        let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
+        let search_view = cx.add_window(|window, cx| {
+            ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None)
+        });
+
+        perform_search(search_view, "let ", cx);
+        let _fake_server = fake_servers.next().await.unwrap();
+        cx.executor().advance_clock(Duration::from_secs(1));
+        cx.executor().run_until_parked();
+        search_view
+            .update(cx, |search_view, _, cx| {
+                assert_eq!(
+                    search_view
+                        .results_editor
+                        .update(cx, |editor, cx| editor.display_text(cx)),
+                    "\n\nfn main() { let a: i32 = 2; }\n"
+                );
+            })
+            .unwrap();
+
+        // Can do the 2nd search without any panics
+        perform_search(search_view, "let ", cx);
+        cx.executor().advance_clock(Duration::from_millis(100));
+        cx.executor().run_until_parked();
+        search_view
+            .update(cx, |search_view, _, cx| {
+                assert_eq!(
+                    search_view
+                        .results_editor
+                        .update(cx, |editor, cx| editor.display_text(cx)),
+                    "\n\nfn main() { let a: i32 = 2; }\n"
+                );
+            })
+            .unwrap();
+    }
+
     fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let settings = SettingsStore::test(cx);

crates/util/src/paths.rs 🔗

@@ -934,7 +934,7 @@ where
 /// 2. When encountering digits, treating consecutive digits as a single number
 /// 3. Comparing numbers by their numeric value rather than lexicographically
 /// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority
-fn natural_sort(a: &str, b: &str) -> Ordering {
+pub fn natural_sort(a: &str, b: &str) -> Ordering {
     let mut a_iter = a.chars().peekable();
     let mut b_iter = b.chars().peekable();
 

crates/vim/src/motion.rs 🔗

@@ -3083,7 +3083,7 @@ mod test {
         state::Mode,
         test::{NeovimBackedTestContext, VimTestContext},
     };
-    use editor::display_map::Inlay;
+    use editor::Inlay;
     use indoc::indoc;
     use language::Point;
     use multi_buffer::MultiBufferRow;