Support basic inlay hints (#2660)

Kirill Bulatov created

Part of https://github.com/zed-industries/community/issues/138
Part of https://linear.app/zed-industries/issue/Z-477/inlay-hints

Supports LSP requests for inlay hints, LSP /refresh request to reload
them.
Reworks DisplayMap and underlying layer to unite suggestions with inlay
hints into new, `InlayMap`.
Adds a hint cache inside `Editor` that tracks buffer/project/LSP request
events, updates the hints and ensures opened editors are showing up to
date text hints on top.

Things left to do after this PR:
* docs on how to configure inlay hints
* blogpost
* dynamic hints: resolve, hover, navigation on click, etc.

Release Notes:

- Added basic support of inlay hints

Change summary

assets/settings/default.json                    |   10 
crates/collab/src/rpc.rs                        |   16 
crates/collab/src/tests/integration_tests.rs    |  584 +++++
crates/editor/src/display_map.rs                |  157 
crates/editor/src/display_map/block_map.rs      |   75 
crates/editor/src/display_map/fold_map.rs       |  606 ++---
crates/editor/src/display_map/inlay_map.rs      | 1697 +++++++++++++++
crates/editor/src/display_map/suggestion_map.rs |  871 --------
crates/editor/src/display_map/tab_map.rs        |  224 -
crates/editor/src/display_map/wrap_map.rs       |   78 
crates/editor/src/editor.rs                     |  249 ++
crates/editor/src/element.rs                    |    9 
crates/editor/src/inlay_hint_cache.rs           | 2021 +++++++++++++++++++
crates/editor/src/multi_buffer/anchor.rs        |   17 
crates/editor/src/scroll.rs                     |   24 
crates/language/src/language_settings.rs        |   61 
crates/lsp/src/lsp.rs                           |   10 
crates/project/src/lsp_command.rs               |  337 +++
crates/project/src/project.rs                   |  279 ++
crates/rope/src/rope.rs                         |    6 
crates/rpc/proto/zed.proto                      |   66 
crates/rpc/src/proto.rs                         |    7 
crates/sum_tree/src/cursor.rs                   |   36 
crates/sum_tree/src/sum_tree.rs                 |   58 
crates/theme/src/theme.rs                       |    1 
styles/src/styleTree/editor.ts                  |    0 
styles/src/style_tree/editor.ts                 |    1 
styles/src/theme/syntax.ts                      |   16 
28 files changed, 5,916 insertions(+), 1,600 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -73,6 +73,16 @@
     // Whether to show git diff indicators in the scrollbar.
     "git_diff": true
   },
+  // Inlay hint related settings
+  "inlay_hints": {
+    // Global switch to toggle hints on and off, switched off by default.
+    "enabled":  false,
+    // Toggle certain types of hints on and off, all switched on by default.
+    "show_type_hints": true,
+    "show_parameter_hints":  true,
+    // Corresponds to null/None LSP hint type value.
+    "show_other_hints": true
+  },
   "project_panel": {
     // Whether to show the git status in the project panel.
     "git_status": true,

crates/collab/src/rpc.rs 🔗

@@ -201,6 +201,7 @@ impl Server {
             .add_message_handler(update_language_server)
             .add_message_handler(update_diagnostic_summary)
             .add_message_handler(update_worktree_settings)
+            .add_message_handler(refresh_inlay_hints)
             .add_request_handler(forward_project_request::<proto::GetHover>)
             .add_request_handler(forward_project_request::<proto::GetDefinition>)
             .add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -226,6 +227,7 @@ impl Server {
             .add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
             .add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
             .add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
+            .add_request_handler(forward_project_request::<proto::InlayHints>)
             .add_message_handler(create_buffer_for_peer)
             .add_request_handler(update_buffer)
             .add_message_handler(update_buffer_file)
@@ -1574,6 +1576,10 @@ async fn update_worktree_settings(
     Ok(())
 }
 
+async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
+    broadcast_project_message(request.project_id, request, session).await
+}
+
 async fn start_language_server(
     request: proto::StartLanguageServer,
     session: Session,
@@ -1750,7 +1756,15 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
 }
 
 async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
+    broadcast_project_message(request.project_id, request, session).await
+}
+
+async fn broadcast_project_message<T: EnvelopedMessage>(
+    project_id: u64,
+    request: T,
+    session: Session,
+) -> Result<()> {
+    let project_id = ProjectId::from_proto(project_id);
     let project_connection_ids = session
         .db()
         .await

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

@@ -18,7 +18,7 @@ use gpui::{
 };
 use indoc::indoc;
 use language::{
-    language_settings::{AllLanguageSettings, Formatter},
+    language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
     tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
     LanguageConfig, OffsetRangeExt, Point, Rope,
 };
@@ -34,7 +34,7 @@ use std::{
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
         Arc,
     },
 };
@@ -7800,6 +7800,572 @@ async fn test_on_input_format_from_guest_to_host(
     });
 }
 
+#[gpui::test]
+async fn test_mutual_editor_inlay_hint_cache_update(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    cx_a.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    cx_b.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+    let language = Arc::new(language);
+    client_a.language_registry.add(Arc::clone(&language));
+    client_b.language_registry.add(language);
+
+    client_a
+        .fs
+        .insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    cx_a.foreground().start_waiting();
+
+    let _buffer_a = project_a
+        .update(cx_a, |project, cx| {
+            project.open_local_buffer("/a/main.rs", cx)
+        })
+        .await
+        .unwrap();
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    let next_call_id = Arc::new(AtomicU32::new(0));
+    let editor_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    fake_language_server
+        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+            let task_next_call_id = Arc::clone(&next_call_id);
+            async move {
+                assert_eq!(
+                    params.text_document.uri,
+                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                );
+                let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+                let mut new_hints = Vec::with_capacity(current_call_id as usize);
+                loop {
+                    new_hints.push(lsp::InlayHint {
+                        position: lsp::Position::new(0, current_call_id),
+                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    });
+                    if current_call_id == 0 {
+                        break;
+                    }
+                    current_call_id -= 1;
+                }
+                Ok(Some(new_hints))
+            }
+        })
+        .next()
+        .await
+        .unwrap();
+
+    cx_a.foreground().finish_waiting();
+    cx_a.foreground().run_until_parked();
+
+    let mut edits_made = 1;
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string()],
+            extract_hint_labels(editor),
+            "Host should get its first hints when opens an editor"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Cache should use editor settings to get the allowed hint kinds"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Host editor update the cache version after every cache/view change",
+        );
+    });
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    cx_b.foreground().run_until_parked();
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string(), "1".to_string()],
+            extract_hint_labels(editor),
+            "Client should get its first hints when opens an editor"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Cache should use editor settings to get the allowed hint kinds"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Guest editor update the cache version after every cache/view change"
+        );
+    });
+
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
+        editor.handle_input(":", cx);
+        cx.focus(&editor_b);
+        edits_made += 1;
+    });
+    cx_a.foreground().run_until_parked();
+    cx_b.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string(), "1".to_string(), "2".to_string()],
+            extract_hint_labels(editor),
+            "Host should get hints from the 1st edit and 1st LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string()
+            ],
+            extract_hint_labels(editor),
+            "Guest should get hints the 1st edit and 2nd LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+
+    editor_a.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+        editor.handle_input("a change to increment both buffers' versions", cx);
+        cx.focus(&editor_a);
+        edits_made += 1;
+    });
+    cx_a.foreground().run_until_parked();
+    cx_b.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string()
+            ],
+            extract_hint_labels(editor),
+            "Host should get hints from 3rd edit, 5th LSP query: \
+4th query was made by guest (but not applied) due to cache invalidation logic"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+                "5".to_string(),
+            ],
+            extract_hint_labels(editor),
+            "Guest should get hints from 3rd edit, 6th LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(inlay_cache.version, edits_made);
+    });
+
+    fake_language_server
+        .request::<lsp::request::InlayHintRefreshRequest>(())
+        .await
+        .expect("inlay refresh request failed");
+    edits_made += 1;
+    cx_a.foreground().run_until_parked();
+    cx_b.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+                "5".to_string(),
+                "6".to_string(),
+            ],
+            extract_hint_labels(editor),
+            "Host should react to /refresh LSP request and get new hints from 7th LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Host should accepted all edits and bump its cache version every time"
+        );
+    });
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec![
+                "0".to_string(),
+                "1".to_string(),
+                "2".to_string(),
+                "3".to_string(),
+                "4".to_string(),
+                "5".to_string(),
+                "6".to_string(),
+                "7".to_string(),
+            ],
+            extract_hint_labels(editor),
+            "Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(
+            inlay_cache.version,
+            edits_made,
+            "Guest should accepted all edits and bump its cache version every time"
+        );
+    });
+}
+
+#[gpui::test]
+async fn test_inlay_hint_refresh_is_forwarded(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    cx_a.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: false,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    cx_b.update(|cx| {
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: true,
+                    show_parameter_hints: false,
+                    show_other_hints: true,
+                })
+            });
+        });
+    });
+    let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+
+    let mut language = Language::new(
+        LanguageConfig {
+            name: "Rust".into(),
+            path_suffixes: vec!["rs".to_string()],
+            ..Default::default()
+        },
+        Some(tree_sitter_rust::language()),
+    );
+    let mut fake_language_servers = language
+        .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        }))
+        .await;
+    let language = Arc::new(language);
+    client_a.language_registry.add(Arc::clone(&language));
+    client_b.language_registry.add(language);
+
+    client_a
+        .fs
+        .insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b);
+    cx_a.foreground().start_waiting();
+    cx_b.foreground().start_waiting();
+
+    let editor_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let fake_language_server = fake_language_servers.next().await.unwrap();
+    let next_call_id = Arc::new(AtomicU32::new(0));
+    fake_language_server
+        .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+            let task_next_call_id = Arc::clone(&next_call_id);
+            async move {
+                assert_eq!(
+                    params.text_document.uri,
+                    lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                );
+                let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
+                let mut new_hints = Vec::with_capacity(current_call_id as usize);
+                loop {
+                    new_hints.push(lsp::InlayHint {
+                        position: lsp::Position::new(0, current_call_id),
+                        label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    });
+                    if current_call_id == 0 {
+                        break;
+                    }
+                    current_call_id -= 1;
+                }
+                Ok(Some(new_hints))
+            }
+        })
+        .next()
+        .await
+        .unwrap();
+    cx_a.foreground().finish_waiting();
+    cx_b.foreground().finish_waiting();
+
+    cx_a.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert!(
+            extract_hint_labels(editor).is_empty(),
+            "Host should get no hints due to them turned off"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Host should have allowed hint kinds set despite hints are off"
+        );
+        assert_eq!(
+            inlay_cache.version, 0,
+            "Host should not increment its cache version due to no changes",
+        );
+    });
+
+    let mut edits_made = 1;
+    cx_b.foreground().run_until_parked();
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string()],
+            extract_hint_labels(editor),
+            "Client should get its first hints when opens an editor"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Cache should use editor settings to get the allowed hint kinds"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Guest editor update the cache version after every cache/view change"
+        );
+    });
+
+    fake_language_server
+        .request::<lsp::request::InlayHintRefreshRequest>(())
+        .await
+        .expect("inlay refresh request failed");
+    cx_a.foreground().run_until_parked();
+    editor_a.update(cx_a, |editor, _| {
+        assert!(
+            extract_hint_labels(editor).is_empty(),
+            "Host should get nop hints due to them turned off, even after the /refresh"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+        assert_eq!(
+            inlay_cache.version, 0,
+            "Host should not increment its cache version due to no changes",
+        );
+    });
+
+    edits_made += 1;
+    cx_b.foreground().run_until_parked();
+    editor_b.update(cx_b, |editor, _| {
+        assert_eq!(
+            vec!["0".to_string(), "1".to_string(),],
+            extract_hint_labels(editor),
+            "Guest should get a /refresh LSP request propagated by host despite host hints are off"
+        );
+        let inlay_cache = editor.inlay_hint_cache();
+        assert_eq!(
+            inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+            "Inlay kinds settings never change during the test"
+        );
+        assert_eq!(
+            inlay_cache.version, edits_made,
+            "Guest should accepted all edits and bump its cache version every time"
+        );
+    });
+}
+
 #[derive(Debug, Eq, PartialEq)]
 struct RoomParticipants {
     remote: Vec<String>,
@@ -7823,3 +8389,17 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
         RoomParticipants { remote, pending }
     })
 }
+
+fn extract_hint_labels(editor: &Editor) -> Vec<String> {
+    let mut labels = Vec::new();
+    for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+        let excerpt_hints = excerpt_hints.read();
+        for (_, inlay) in excerpt_hints.hints.iter() {
+            match &inlay.label {
+                project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+                _ => unreachable!(),
+            }
+        }
+    }
+    labels
+}

crates/editor/src/display_map.rs 🔗

@@ -1,26 +1,26 @@
 mod block_map;
 mod fold_map;
-mod suggestion_map;
+mod inlay_map;
 mod tab_map;
 mod wrap_map;
 
-use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
 pub use block_map::{BlockMap, BlockPoint};
 use collections::{HashMap, HashSet};
-use fold_map::{FoldMap, FoldOffset};
+use fold_map::FoldMap;
 use gpui::{
     color::Color,
     fonts::{FontId, HighlightStyle},
     Entity, ModelContext, ModelHandle,
 };
+use inlay_map::InlayMap;
 use language::{
     language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
 };
 use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
-pub use suggestion_map::Suggestion;
-use suggestion_map::SuggestionMap;
 use sum_tree::{Bias, TreeMap};
 use tab_map::TabMap;
+use text::Rope;
 use wrap_map::WrapMap;
 
 pub use block_map::{
@@ -28,6 +28,8 @@ pub use block_map::{
     BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
 };
 
+pub use self::inlay_map::{Inlay, InlayProperties};
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum FoldStatus {
     Folded,
@@ -44,7 +46,7 @@ pub struct DisplayMap {
     buffer: ModelHandle<MultiBuffer>,
     buffer_subscription: BufferSubscription,
     fold_map: FoldMap,
-    suggestion_map: SuggestionMap,
+    inlay_map: InlayMap,
     tab_map: TabMap,
     wrap_map: ModelHandle<WrapMap>,
     block_map: BlockMap,
@@ -69,8 +71,8 @@ impl DisplayMap {
         let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
 
         let tab_size = Self::tab_size(&buffer, cx);
-        let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
-        let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
+        let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        let (fold_map, snapshot) = FoldMap::new(snapshot);
         let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
         let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
         let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
@@ -79,7 +81,7 @@ impl DisplayMap {
             buffer,
             buffer_subscription,
             fold_map,
-            suggestion_map,
+            inlay_map,
             tab_map,
             wrap_map,
             block_map,
@@ -88,16 +90,13 @@ impl DisplayMap {
         }
     }
 
-    pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
+    pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
         let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
-        let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
-        let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
-
+        let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+        let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
         let tab_size = Self::tab_size(&self.buffer, cx);
-        let (tab_snapshot, edits) = self
-            .tab_map
-            .sync(suggestion_snapshot.clone(), edits, tab_size);
+        let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
         let (wrap_snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
@@ -106,7 +105,7 @@ impl DisplayMap {
         DisplaySnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(cx),
             fold_snapshot,
-            suggestion_snapshot,
+            inlay_snapshot,
             tab_snapshot,
             wrap_snapshot,
             block_snapshot,
@@ -132,15 +131,14 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.fold(ranges);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -157,15 +155,14 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
             .update(cx, |map, cx| map.sync(snapshot, edits, cx));
         self.block_map.read(snapshot, edits);
         let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -181,8 +178,8 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -199,8 +196,8 @@ impl DisplayMap {
         let snapshot = self.buffer.read(cx).snapshot(cx);
         let edits = self.buffer_subscription.consume().into_inner();
         let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
         let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
         let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
         let (snapshot, edits) = self
             .wrap_map
@@ -231,32 +228,6 @@ impl DisplayMap {
         self.text_highlights.remove(&Some(type_id))
     }
 
-    pub fn has_suggestion(&self) -> bool {
-        self.suggestion_map.has_suggestion()
-    }
-
-    pub fn replace_suggestion<T>(
-        &self,
-        new_suggestion: Option<Suggestion<T>>,
-        cx: &mut ModelContext<Self>,
-    ) -> Option<Suggestion<FoldOffset>>
-    where
-        T: ToPoint,
-    {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        let edits = self.buffer_subscription.consume().into_inner();
-        let tab_size = Self::tab_size(&self.buffer, cx);
-        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
-        let (snapshot, edits, old_suggestion) =
-            self.suggestion_map.replace(new_suggestion, snapshot, edits);
-        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
-        let (snapshot, edits) = self
-            .wrap_map
-            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
-        self.block_map.read(snapshot, edits);
-        old_suggestion
-    }
-
     pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_font(font_id, font_size, cx))
@@ -271,6 +242,39 @@ impl DisplayMap {
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
     }
 
+    pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+        self.inlay_map.current_inlays()
+    }
+
+    pub fn splice_inlays<T: Into<Rope>>(
+        &mut self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<(InlayId, InlayProperties<T>)>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if to_remove.is_empty() && to_insert.is_empty() {
+            return;
+        }
+        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+        let edits = self.buffer_subscription.consume().into_inner();
+        let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let tab_size = Self::tab_size(&self.buffer, cx);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+
+        let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
+        let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+        let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
+        let (snapshot, edits) = self
+            .wrap_map
+            .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+        self.block_map.read(snapshot, edits);
+    }
+
     fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
         let language = buffer
             .read(cx)
@@ -288,7 +292,7 @@ impl DisplayMap {
 pub struct DisplaySnapshot {
     pub buffer_snapshot: MultiBufferSnapshot,
     fold_snapshot: fold_map::FoldSnapshot,
-    suggestion_snapshot: suggestion_map::SuggestionSnapshot,
+    inlay_snapshot: inlay_map::InlaySnapshot,
     tab_snapshot: tab_map::TabSnapshot,
     wrap_snapshot: wrap_map::WrapSnapshot,
     block_snapshot: block_map::BlockSnapshot,
@@ -316,9 +320,11 @@ impl DisplaySnapshot {
 
     pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
-            *fold_point.column_mut() = 0;
-            point = fold_point.to_buffer_point(&self.fold_snapshot);
+            let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+            let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
+            fold_point.0.column = 0;
+            inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+            point = self.inlay_snapshot.to_buffer_point(inlay_point);
 
             let mut display_point = self.point_to_display_point(point, Bias::Left);
             *display_point.column_mut() = 0;
@@ -332,9 +338,11 @@ impl DisplaySnapshot {
 
     pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
         loop {
-            let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
-            *fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
-            point = fold_point.to_buffer_point(&self.fold_snapshot);
+            let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
+            let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
+            fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
+            inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+            point = self.inlay_snapshot.to_buffer_point(inlay_point);
 
             let mut display_point = self.point_to_display_point(point, Bias::Right);
             *display_point.column_mut() = self.line_len(display_point.row());
@@ -364,9 +372,9 @@ impl DisplaySnapshot {
     }
 
     fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
-        let fold_point = self.fold_snapshot.to_fold_point(point, bias);
-        let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
-        let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
+        let inlay_point = self.inlay_snapshot.to_inlay_point(point);
+        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+        let tab_point = self.tab_snapshot.to_tab_point(fold_point);
         let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
         let block_point = self.block_snapshot.to_block_point(wrap_point);
         DisplayPoint(block_point)
@@ -376,9 +384,9 @@ impl DisplaySnapshot {
         let block_point = point.0;
         let wrap_point = self.block_snapshot.to_wrap_point(block_point);
         let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
-        let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
-        let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
-        fold_point.to_buffer_point(&self.fold_snapshot)
+        let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+        self.inlay_snapshot.to_buffer_point(inlay_point)
     }
 
     pub fn max_point(&self) -> DisplayPoint {
@@ -388,7 +396,13 @@ impl DisplaySnapshot {
     /// Returns text chunks starting at the given display row until the end of the file
     pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         self.block_snapshot
-            .chunks(display_row..self.max_point().row() + 1, false, None, None)
+            .chunks(
+                display_row..self.max_point().row() + 1,
+                false,
+                None,
+                None,
+                None,
+            )
             .map(|h| h.text)
     }
 
@@ -396,7 +410,7 @@ impl DisplaySnapshot {
     pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
         (0..=display_row).into_iter().rev().flat_map(|row| {
             self.block_snapshot
-                .chunks(row..row + 1, false, None, None)
+                .chunks(row..row + 1, false, None, None, None)
                 .map(|h| h.text)
                 .collect::<Vec<_>>()
                 .into_iter()
@@ -408,13 +422,15 @@ impl DisplaySnapshot {
         &self,
         display_rows: Range<u32>,
         language_aware: bool,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> DisplayChunks<'_> {
         self.block_snapshot.chunks(
             display_rows,
             language_aware,
             Some(&self.text_highlights),
-            suggestion_highlight,
+            hint_highlights,
+            suggestion_highlights,
         )
     }
 
@@ -790,9 +806,10 @@ impl DisplayPoint {
     pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
         let wrap_point = map.block_snapshot.to_wrap_point(self.0);
         let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
-        let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
-        let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
-        fold_point.to_buffer_offset(&map.fold_snapshot)
+        let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
+        map.inlay_snapshot
+            .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
     }
 }
 
@@ -1706,7 +1723,7 @@ pub mod tests {
     ) -> Vec<(String, Option<Color>, Option<Color>)> {
         let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
         let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
-        for chunk in snapshot.chunks(rows, true, None) {
+        for chunk in snapshot.chunks(rows, true, None, None) {
             let syntax_color = chunk
                 .syntax_highlight_id
                 .and_then(|id| id.style(theme)?.color);

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

@@ -573,9 +573,15 @@ impl<'a> BlockMapWriter<'a> {
 impl BlockSnapshot {
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(0..self.transforms.summary().output_rows, false, None, None)
-            .map(|chunk| chunk.text)
-            .collect()
+        self.chunks(
+            0..self.transforms.summary().output_rows,
+            false,
+            None,
+            None,
+            None,
+        )
+        .map(|chunk| chunk.text)
+        .collect()
     }
 
     pub fn chunks<'a>(
@@ -583,7 +589,8 @@ impl BlockSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> BlockChunks<'a> {
         let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
         let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
@@ -616,7 +623,8 @@ impl BlockSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                suggestion_highlight,
+                hint_highlights,
+                suggestion_highlights,
             ),
             input_chunk: Default::default(),
             transforms: cursor,
@@ -989,7 +997,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::display_map::suggestion_map::SuggestionMap;
+    use crate::display_map::inlay_map::InlayMap;
     use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
     use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
@@ -1030,9 +1038,9 @@ mod tests {
         let buffer = MultiBuffer::build_simple(text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
-        let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
         let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
 
@@ -1175,12 +1183,11 @@ mod tests {
             buffer.snapshot(cx)
         });
 
-        let (fold_snapshot, fold_edits) =
-            fold_map.read(buffer_snapshot, subscription.consume().into_inner());
-        let (suggestion_snapshot, suggestion_edits) =
-            suggestion_map.sync(fold_snapshot, fold_edits);
+        let (inlay_snapshot, inlay_edits) =
+            inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+        let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
         let (tab_snapshot, tab_edits) =
-            tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
+            tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
         let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
             wrap_map.sync(tab_snapshot, tab_edits, cx)
         });
@@ -1205,9 +1212,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
         let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
         let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
 
@@ -1277,9 +1284,9 @@ mod tests {
         };
 
         let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
         let (wrap_map, wraps_snapshot) =
             WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
         let mut block_map = BlockMap::new(
@@ -1332,12 +1339,11 @@ mod tests {
                         })
                         .collect::<Vec<_>>();
 
-                    let (fold_snapshot, fold_edits) =
-                        fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (suggestion_snapshot, suggestion_edits) =
-                        suggestion_map.sync(fold_snapshot, fold_edits);
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tab_snapshot, tab_edits) =
-                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tab_snapshot, tab_edits, cx)
                     });
@@ -1357,12 +1363,11 @@ mod tests {
                         })
                         .collect();
 
-                    let (fold_snapshot, fold_edits) =
-                        fold_map.read(buffer_snapshot.clone(), vec![]);
-                    let (suggestion_snapshot, suggestion_edits) =
-                        suggestion_map.sync(fold_snapshot, fold_edits);
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.sync(buffer_snapshot.clone(), vec![]);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tab_snapshot, tab_edits) =
-                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                         wrap_map.sync(tab_snapshot, tab_edits, cx)
                     });
@@ -1381,11 +1386,10 @@ mod tests {
                 }
             }
 
-            let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            let (suggestion_snapshot, suggestion_edits) =
-                suggestion_map.sync(fold_snapshot, fold_edits);
-            let (tab_snapshot, tab_edits) =
-                tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+            let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
             let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
                 wrap_map.sync(tab_snapshot, tab_edits, cx)
             });
@@ -1499,6 +1503,7 @@ mod tests {
                         false,
                         None,
                         None,
+                        None,
                     )
                     .map(|chunk| chunk.text)
                     .collect::<String>();

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

@@ -1,19 +1,15 @@
-use super::TextHighlights;
-use crate::{
-    multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot,
-    ToOffset,
+use super::{
+    inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
+    TextHighlights,
 };
-use collections::BTreeMap;
+use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
 use gpui::{color::Color, fonts::HighlightStyle};
 use language::{Chunk, Edit, Point, TextSummary};
-use parking_lot::Mutex;
 use std::{
     any::TypeId,
     cmp::{self, Ordering},
-    iter::{self, Peekable},
-    ops::{Range, Sub},
-    sync::atomic::{AtomicUsize, Ordering::SeqCst},
-    vec,
+    iter,
+    ops::{Add, AddAssign, Range, Sub},
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
 
@@ -29,28 +25,24 @@ impl FoldPoint {
         self.0.row
     }
 
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
     pub fn row_mut(&mut self) -> &mut u32 {
         &mut self.0.row
     }
 
+    #[cfg(test)]
     pub fn column_mut(&mut self) -> &mut u32 {
         &mut self.0.column
     }
 
-    pub fn to_buffer_point(self, snapshot: &FoldSnapshot) -> Point {
-        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
-        cursor.seek(&self, Bias::Right, &());
-        let overshoot = self.0 - cursor.start().0 .0;
-        cursor.start().1 + overshoot
-    }
-
-    pub fn to_buffer_offset(self, snapshot: &FoldSnapshot) -> usize {
-        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>();
+    pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
+        let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&self, Bias::Right, &());
         let overshoot = self.0 - cursor.start().0 .0;
-        snapshot
-            .buffer_snapshot
-            .point_to_offset(cursor.start().1 + overshoot)
+        InlayPoint(cursor.start().1 .0 + overshoot)
     }
 
     pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
@@ -63,10 +55,10 @@ impl FoldPoint {
         if !overshoot.is_zero() {
             let transform = cursor.item().expect("display point out of range");
             assert!(transform.output_text.is_none());
-            let end_buffer_offset = snapshot
-                .buffer_snapshot
-                .point_to_offset(cursor.start().1.input.lines + overshoot);
-            offset += end_buffer_offset - cursor.start().1.input.len;
+            let end_inlay_offset = snapshot
+                .inlay_snapshot
+                .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
+            offset += end_inlay_offset.0 - cursor.start().1.input.len;
         }
         FoldOffset(offset)
     }
@@ -87,8 +79,9 @@ impl<'a> FoldMapWriter<'a> {
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
         let mut edits = Vec::new();
         let mut folds = Vec::new();
-        let buffer = self.0.buffer.lock().clone();
+        let snapshot = self.0.snapshot.inlay_snapshot.clone();
         for range in ranges.into_iter() {
+            let buffer = &snapshot.buffer;
             let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
 
             // Ignore any empty ranges.
@@ -103,35 +96,32 @@ impl<'a> FoldMapWriter<'a> {
             }
 
             folds.push(fold);
-            edits.push(text::Edit {
-                old: range.clone(),
-                new: range,
+
+            let inlay_range =
+                snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
+            edits.push(InlayEdit {
+                old: inlay_range.clone(),
+                new: inlay_range,
             });
         }
 
-        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer));
+        let buffer = &snapshot.buffer;
+        folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
 
-        self.0.folds = {
+        self.0.snapshot.folds = {
             let mut new_tree = SumTree::new();
-            let mut cursor = self.0.folds.cursor::<Fold>();
+            let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
             for fold in folds {
-                new_tree.append(cursor.slice(&fold, Bias::Right, &buffer), &buffer);
-                new_tree.push(fold, &buffer);
+                new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+                new_tree.push(fold, buffer);
             }
-            new_tree.append(cursor.suffix(&buffer), &buffer);
+            new_tree.append(cursor.suffix(buffer), buffer);
             new_tree
         };
 
-        consolidate_buffer_edits(&mut edits);
-        let edits = self.0.sync(buffer.clone(), edits);
-        let snapshot = FoldSnapshot {
-            transforms: self.0.transforms.lock().clone(),
-            folds: self.0.folds.clone(),
-            buffer_snapshot: buffer,
-            version: self.0.version.load(SeqCst),
-            ellipses_color: self.0.ellipses_color,
-        };
-        (snapshot, edits)
+        consolidate_inlay_edits(&mut edits);
+        let edits = self.0.sync(snapshot.clone(), edits);
+        (self.0.snapshot.clone(), edits)
     }
 
     pub fn unfold<T: ToOffset>(
@@ -141,110 +131,93 @@ impl<'a> FoldMapWriter<'a> {
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
         let mut edits = Vec::new();
         let mut fold_ixs_to_delete = Vec::new();
-        let buffer = self.0.buffer.lock().clone();
+        let snapshot = self.0.snapshot.inlay_snapshot.clone();
+        let buffer = &snapshot.buffer;
         for range in ranges.into_iter() {
             // Remove intersecting folds and add their ranges to edits that are passed to sync.
-            let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive);
+            let mut folds_cursor =
+                intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
             while let Some(fold) = folds_cursor.item() {
-                let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer);
+                let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
                 if offset_range.end > offset_range.start {
-                    edits.push(text::Edit {
-                        old: offset_range.clone(),
-                        new: offset_range,
+                    let inlay_range = snapshot.to_inlay_offset(offset_range.start)
+                        ..snapshot.to_inlay_offset(offset_range.end);
+                    edits.push(InlayEdit {
+                        old: inlay_range.clone(),
+                        new: inlay_range,
                     });
                 }
                 fold_ixs_to_delete.push(*folds_cursor.start());
-                folds_cursor.next(&buffer);
+                folds_cursor.next(buffer);
             }
         }
 
         fold_ixs_to_delete.sort_unstable();
         fold_ixs_to_delete.dedup();
 
-        self.0.folds = {
-            let mut cursor = self.0.folds.cursor::<usize>();
+        self.0.snapshot.folds = {
+            let mut cursor = self.0.snapshot.folds.cursor::<usize>();
             let mut folds = SumTree::new();
             for fold_ix in fold_ixs_to_delete {
-                folds.append(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer);
-                cursor.next(&buffer);
+                folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
+                cursor.next(buffer);
             }
-            folds.append(cursor.suffix(&buffer), &buffer);
+            folds.append(cursor.suffix(buffer), buffer);
             folds
         };
 
-        consolidate_buffer_edits(&mut edits);
-        let edits = self.0.sync(buffer.clone(), edits);
-        let snapshot = FoldSnapshot {
-            transforms: self.0.transforms.lock().clone(),
-            folds: self.0.folds.clone(),
-            buffer_snapshot: buffer,
-            version: self.0.version.load(SeqCst),
-            ellipses_color: self.0.ellipses_color,
-        };
-        (snapshot, edits)
+        consolidate_inlay_edits(&mut edits);
+        let edits = self.0.sync(snapshot.clone(), edits);
+        (self.0.snapshot.clone(), edits)
     }
 }
 
 pub struct FoldMap {
-    buffer: Mutex<MultiBufferSnapshot>,
-    transforms: Mutex<SumTree<Transform>>,
-    folds: SumTree<Fold>,
-    version: AtomicUsize,
+    snapshot: FoldSnapshot,
     ellipses_color: Option<Color>,
 }
 
 impl FoldMap {
-    pub fn new(buffer: MultiBufferSnapshot) -> (Self, FoldSnapshot) {
+    pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
         let this = Self {
-            buffer: Mutex::new(buffer.clone()),
-            folds: Default::default(),
-            transforms: Mutex::new(SumTree::from_item(
-                Transform {
-                    summary: TransformSummary {
-                        input: buffer.text_summary(),
-                        output: buffer.text_summary(),
+            snapshot: FoldSnapshot {
+                folds: Default::default(),
+                transforms: SumTree::from_item(
+                    Transform {
+                        summary: TransformSummary {
+                            input: inlay_snapshot.text_summary(),
+                            output: inlay_snapshot.text_summary(),
+                        },
+                        output_text: None,
                     },
-                    output_text: None,
-                },
-                &(),
-            )),
-            ellipses_color: None,
-            version: Default::default(),
-        };
-
-        let snapshot = FoldSnapshot {
-            transforms: this.transforms.lock().clone(),
-            folds: this.folds.clone(),
-            buffer_snapshot: this.buffer.lock().clone(),
-            version: this.version.load(SeqCst),
+                    &(),
+                ),
+                inlay_snapshot: inlay_snapshot.clone(),
+                version: 0,
+                ellipses_color: None,
+            },
             ellipses_color: None,
         };
+        let snapshot = this.snapshot.clone();
         (this, snapshot)
     }
 
     pub fn read(
-        &self,
-        buffer: MultiBufferSnapshot,
-        edits: Vec<Edit<usize>>,
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        edits: Vec<InlayEdit>,
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
-        let edits = self.sync(buffer, edits);
+        let edits = self.sync(inlay_snapshot, edits);
         self.check_invariants();
-        let snapshot = FoldSnapshot {
-            transforms: self.transforms.lock().clone(),
-            folds: self.folds.clone(),
-            buffer_snapshot: self.buffer.lock().clone(),
-            version: self.version.load(SeqCst),
-            ellipses_color: self.ellipses_color,
-        };
-        (snapshot, edits)
+        (self.snapshot.clone(), edits)
     }
 
     pub fn write(
         &mut self,
-        buffer: MultiBufferSnapshot,
-        edits: Vec<Edit<usize>>,
+        inlay_snapshot: InlaySnapshot,
+        edits: Vec<InlayEdit>,
     ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
-        let (snapshot, edits) = self.read(buffer, edits);
+        let (snapshot, edits) = self.read(inlay_snapshot, edits);
         (FoldMapWriter(self), snapshot, edits)
     }
 
@@ -260,15 +233,17 @@ impl FoldMap {
     fn check_invariants(&self) {
         if cfg!(test) {
             assert_eq!(
-                self.transforms.lock().summary().input.len,
-                self.buffer.lock().len(),
-                "transform tree does not match buffer's length"
+                self.snapshot.transforms.summary().input.len,
+                self.snapshot.inlay_snapshot.len().0,
+                "transform tree does not match inlay snapshot's length"
             );
 
-            let mut folds = self.folds.iter().peekable();
+            let mut folds = self.snapshot.folds.iter().peekable();
             while let Some(fold) = folds.next() {
                 if let Some(next_fold) = folds.peek() {
-                    let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock());
+                    let comparison = fold
+                        .0
+                        .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
                     assert!(comparison.is_le());
                 }
             }
@@ -276,50 +251,42 @@ impl FoldMap {
     }
 
     fn sync(
-        &self,
-        new_buffer: MultiBufferSnapshot,
-        buffer_edits: Vec<text::Edit<usize>>,
+        &mut self,
+        inlay_snapshot: InlaySnapshot,
+        inlay_edits: Vec<InlayEdit>,
     ) -> Vec<FoldEdit> {
-        if buffer_edits.is_empty() {
-            let mut buffer = self.buffer.lock();
-            if buffer.edit_count() != new_buffer.edit_count()
-                || buffer.parse_count() != new_buffer.parse_count()
-                || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
-                || buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
-                || buffer.trailing_excerpt_update_count()
-                    != new_buffer.trailing_excerpt_update_count()
-            {
-                self.version.fetch_add(1, SeqCst);
+        if inlay_edits.is_empty() {
+            if self.snapshot.inlay_snapshot.version != inlay_snapshot.version {
+                self.snapshot.version += 1;
             }
-            *buffer = new_buffer;
+            self.snapshot.inlay_snapshot = inlay_snapshot;
             Vec::new()
         } else {
-            let mut buffer_edits_iter = buffer_edits.iter().cloned().peekable();
+            let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
 
             let mut new_transforms = SumTree::new();
-            let mut transforms = self.transforms.lock();
-            let mut cursor = transforms.cursor::<usize>();
-            cursor.seek(&0, Bias::Right, &());
+            let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
+            cursor.seek(&InlayOffset(0), Bias::Right, &());
 
-            while let Some(mut edit) = buffer_edits_iter.next() {
+            while let Some(mut edit) = inlay_edits_iter.next() {
                 new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
-                edit.new.start -= edit.old.start - cursor.start();
+                edit.new.start -= edit.old.start - *cursor.start();
                 edit.old.start = *cursor.start();
 
                 cursor.seek(&edit.old.end, Bias::Right, &());
                 cursor.next(&());
 
-                let mut delta = edit.new.len() as isize - edit.old.len() as isize;
+                let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
                 loop {
                     edit.old.end = *cursor.start();
 
-                    if let Some(next_edit) = buffer_edits_iter.peek() {
+                    if let Some(next_edit) = inlay_edits_iter.peek() {
                         if next_edit.old.start > edit.old.end {
                             break;
                         }
 
-                        let next_edit = buffer_edits_iter.next().unwrap();
-                        delta += next_edit.new.len() as isize - next_edit.old.len() as isize;
+                        let next_edit = inlay_edits_iter.next().unwrap();
+                        delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize;
 
                         if next_edit.old.end >= edit.old.end {
                             edit.old.end = next_edit.old.end;
@@ -331,19 +298,29 @@ impl FoldMap {
                     }
                 }
 
-                edit.new.end = ((edit.new.start + edit.old.len()) as isize + delta) as usize;
-
-                let anchor = new_buffer.anchor_before(edit.new.start);
-                let mut folds_cursor = self.folds.cursor::<Fold>();
-                folds_cursor.seek(&Fold(anchor..Anchor::max()), Bias::Left, &new_buffer);
+                edit.new.end =
+                    InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
+
+                let anchor = inlay_snapshot
+                    .buffer
+                    .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
+                let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+                folds_cursor.seek(
+                    &Fold(anchor..Anchor::max()),
+                    Bias::Left,
+                    &inlay_snapshot.buffer,
+                );
 
                 let mut folds = iter::from_fn({
-                    let buffer = &new_buffer;
+                    let inlay_snapshot = &inlay_snapshot;
                     move || {
-                        let item = folds_cursor
-                            .item()
-                            .map(|f| f.0.start.to_offset(buffer)..f.0.end.to_offset(buffer));
-                        folds_cursor.next(buffer);
+                        let item = folds_cursor.item().map(|f| {
+                            let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
+                            let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+                            inlay_snapshot.to_inlay_offset(buffer_start)
+                                ..inlay_snapshot.to_inlay_offset(buffer_end)
+                        });
+                        folds_cursor.next(&inlay_snapshot.buffer);
                         item
                     }
                 })
@@ -353,7 +330,7 @@ impl FoldMap {
                     let mut fold = folds.next().unwrap();
                     let sum = new_transforms.summary();
 
-                    assert!(fold.start >= sum.input.len);
+                    assert!(fold.start.0 >= sum.input.len);
 
                     while folds
                         .peek()
@@ -365,9 +342,9 @@ impl FoldMap {
                         }
                     }
 
-                    if fold.start > sum.input.len {
-                        let text_summary = new_buffer
-                            .text_summary_for_range::<TextSummary, _>(sum.input.len..fold.start);
+                    if fold.start.0 > sum.input.len {
+                        let text_summary = inlay_snapshot
+                            .text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
                         new_transforms.push(
                             Transform {
                                 summary: TransformSummary {
@@ -386,7 +363,8 @@ impl FoldMap {
                             Transform {
                                 summary: TransformSummary {
                                     output: TextSummary::from(output_text),
-                                    input: new_buffer.text_summary_for_range(fold.start..fold.end),
+                                    input: inlay_snapshot
+                                        .text_summary_for_range(fold.start..fold.end),
                                 },
                                 output_text: Some(output_text),
                             },
@@ -396,9 +374,9 @@ impl FoldMap {
                 }
 
                 let sum = new_transforms.summary();
-                if sum.input.len < edit.new.end {
-                    let text_summary = new_buffer
-                        .text_summary_for_range::<TextSummary, _>(sum.input.len..edit.new.end);
+                if sum.input.len < edit.new.end.0 {
+                    let text_summary = inlay_snapshot
+                        .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
                     new_transforms.push(
                         Transform {
                             summary: TransformSummary {
@@ -414,7 +392,7 @@ impl FoldMap {
 
             new_transforms.append(cursor.suffix(&()), &());
             if new_transforms.is_empty() {
-                let text_summary = new_buffer.text_summary();
+                let text_summary = inlay_snapshot.text_summary();
                 new_transforms.push(
                     Transform {
                         summary: TransformSummary {
@@ -429,18 +407,21 @@ impl FoldMap {
 
             drop(cursor);
 
-            let mut fold_edits = Vec::with_capacity(buffer_edits.len());
+            let mut fold_edits = Vec::with_capacity(inlay_edits.len());
             {
-                let mut old_transforms = transforms.cursor::<(usize, FoldOffset)>();
-                let mut new_transforms = new_transforms.cursor::<(usize, FoldOffset)>();
+                let mut old_transforms = self
+                    .snapshot
+                    .transforms
+                    .cursor::<(InlayOffset, FoldOffset)>();
+                let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>();
 
-                for mut edit in buffer_edits {
+                for mut edit in inlay_edits {
                     old_transforms.seek(&edit.old.start, Bias::Left, &());
                     if old_transforms.item().map_or(false, |t| t.is_fold()) {
                         edit.old.start = old_transforms.start().0;
                     }
                     let old_start =
-                        old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0);
+                        old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0;
 
                     old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
                     if old_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -448,14 +429,14 @@ impl FoldMap {
                         edit.old.end = old_transforms.start().0;
                     }
                     let old_end =
-                        old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0);
+                        old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0;
 
                     new_transforms.seek(&edit.new.start, Bias::Left, &());
                     if new_transforms.item().map_or(false, |t| t.is_fold()) {
                         edit.new.start = new_transforms.start().0;
                     }
                     let new_start =
-                        new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0);
+                        new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0;
 
                     new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
                     if new_transforms.item().map_or(false, |t| t.is_fold()) {
@@ -463,7 +444,7 @@ impl FoldMap {
                         edit.new.end = new_transforms.start().0;
                     }
                     let new_end =
-                        new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0);
+                        new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0;
 
                     fold_edits.push(FoldEdit {
                         old: FoldOffset(old_start)..FoldOffset(old_end),
@@ -474,9 +455,9 @@ impl FoldMap {
                 consolidate_fold_edits(&mut fold_edits);
             }
 
-            *transforms = new_transforms;
-            *self.buffer.lock() = new_buffer;
-            self.version.fetch_add(1, SeqCst);
+            self.snapshot.transforms = new_transforms;
+            self.snapshot.inlay_snapshot = inlay_snapshot;
+            self.snapshot.version += 1;
             fold_edits
         }
     }
@@ -486,32 +467,28 @@ impl FoldMap {
 pub struct FoldSnapshot {
     transforms: SumTree<Transform>,
     folds: SumTree<Fold>,
-    buffer_snapshot: MultiBufferSnapshot,
+    pub inlay_snapshot: InlaySnapshot,
     pub version: usize,
     pub ellipses_color: Option<Color>,
 }
 
 impl FoldSnapshot {
-    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        &self.buffer_snapshot
-    }
-
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(FoldOffset(0)..self.len(), false, None)
+        self.chunks(FoldOffset(0)..self.len(), false, None, None, None)
             .map(|c| c.text)
             .collect()
     }
 
     #[cfg(test)]
     pub fn fold_count(&self) -> usize {
-        self.folds.items(&self.buffer_snapshot).len()
+        self.folds.items(&self.inlay_snapshot.buffer).len()
     }
 
     pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
         let mut summary = TextSummary::default();
 
-        let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&range.start, Bias::Right, &());
         if let Some(transform) = cursor.item() {
             let start_in_transform = range.start.0 - cursor.start().0 .0;
@@ -522,11 +499,15 @@ impl FoldSnapshot {
                         [start_in_transform.column as usize..end_in_transform.column as usize],
                 );
             } else {
-                let buffer_start = cursor.start().1 + start_in_transform;
-                let buffer_end = cursor.start().1 + end_in_transform;
+                let inlay_start = self
+                    .inlay_snapshot
+                    .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform));
+                let inlay_end = self
+                    .inlay_snapshot
+                    .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
                 summary = self
-                    .buffer_snapshot
-                    .text_summary_for_range(buffer_start..buffer_end);
+                    .inlay_snapshot
+                    .text_summary_for_range(inlay_start..inlay_end);
             }
         }
 
@@ -540,11 +521,13 @@ impl FoldSnapshot {
                 if let Some(output_text) = transform.output_text {
                     summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
                 } else {
-                    let buffer_start = cursor.start().1;
-                    let buffer_end = cursor.start().1 + end_in_transform;
+                    let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
+                    let inlay_end = self
+                        .inlay_snapshot
+                        .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
                     summary += self
-                        .buffer_snapshot
-                        .text_summary_for_range::<TextSummary, _>(buffer_start..buffer_end);
+                        .inlay_snapshot
+                        .text_summary_for_range(inlay_start..inlay_end);
                 }
             }
         }
@@ -552,8 +535,8 @@ impl FoldSnapshot {
         summary
     }
 
-    pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
-        let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>();
+    pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>();
         cursor.seek(&point, Bias::Right, &());
         if cursor.item().map_or(false, |t| t.is_fold()) {
             if bias == Bias::Left || point == cursor.start().0 {
@@ -562,7 +545,7 @@ impl FoldSnapshot {
                 cursor.end(&()).1
             }
         } else {
-            let overshoot = point - cursor.start().0;
+            let overshoot = point.0 - cursor.start().0 .0;
             FoldPoint(cmp::min(
                 cursor.start().1 .0 + overshoot,
                 cursor.end(&()).1 .0,
@@ -590,12 +573,12 @@ impl FoldSnapshot {
         }
 
         let fold_point = FoldPoint::new(start_row, 0);
-        let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&fold_point, Bias::Left, &());
 
         let overshoot = fold_point.0 - cursor.start().0 .0;
-        let buffer_point = cursor.start().1 + overshoot;
-        let input_buffer_rows = self.buffer_snapshot.buffer_rows(buffer_point.row);
+        let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
+        let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
 
         FoldBufferRows {
             fold_point,
@@ -617,10 +600,10 @@ impl FoldSnapshot {
     where
         T: ToOffset,
     {
-        let mut folds = intersecting_folds(&self.buffer_snapshot, &self.folds, range, false);
+        let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
         iter::from_fn(move || {
             let item = folds.item().map(|f| &f.0);
-            folds.next(&self.buffer_snapshot);
+            folds.next(&self.inlay_snapshot.buffer);
             item
         })
     }
@@ -629,26 +612,39 @@ impl FoldSnapshot {
     where
         T: ToOffset,
     {
-        let offset = offset.to_offset(&self.buffer_snapshot);
-        let mut cursor = self.transforms.cursor::<usize>();
-        cursor.seek(&offset, Bias::Right, &());
+        let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
+        let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
+        let mut cursor = self.transforms.cursor::<InlayOffset>();
+        cursor.seek(&inlay_offset, Bias::Right, &());
         cursor.item().map_or(false, |t| t.output_text.is_some())
     }
 
     pub fn is_line_folded(&self, buffer_row: u32) -> bool {
-        let mut cursor = self.transforms.cursor::<Point>();
-        cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &());
-        while let Some(transform) = cursor.item() {
-            if transform.output_text.is_some() {
-                return true;
+        let mut inlay_point = self
+            .inlay_snapshot
+            .to_inlay_point(Point::new(buffer_row, 0));
+        let mut cursor = self.transforms.cursor::<InlayPoint>();
+        cursor.seek(&inlay_point, Bias::Right, &());
+        loop {
+            match cursor.item() {
+                Some(transform) => {
+                    let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
+                    if buffer_point.row != buffer_row {
+                        return false;
+                    } else if transform.output_text.is_some() {
+                        return true;
+                    }
+                }
+                None => return false,
             }
-            if cursor.end(&()).row == buffer_row {
-                cursor.next(&())
+
+            if cursor.end(&()).row() == inlay_point.row() {
+                cursor.next(&());
             } else {
-                break;
+                inlay_point.0 += Point::new(1, 0);
+                cursor.seek(&inlay_point, Bias::Right, &());
             }
         }
-        false
     }
 
     pub fn chunks<'a>(
@@ -656,127 +652,56 @@ impl FoldSnapshot {
         range: Range<FoldOffset>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> FoldChunks<'a> {
-        let mut highlight_endpoints = Vec::new();
-        let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>();
+        let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
 
-        let buffer_end = {
+        let inlay_end = {
             transform_cursor.seek(&range.end, Bias::Right, &());
             let overshoot = range.end.0 - transform_cursor.start().0 .0;
-            transform_cursor.start().1 + overshoot
+            transform_cursor.start().1 + InlayOffset(overshoot)
         };
 
-        let buffer_start = {
+        let inlay_start = {
             transform_cursor.seek(&range.start, Bias::Right, &());
             let overshoot = range.start.0 - transform_cursor.start().0 .0;
-            transform_cursor.start().1 + overshoot
+            transform_cursor.start().1 + InlayOffset(overshoot)
         };
 
-        if let Some(text_highlights) = text_highlights {
-            if !text_highlights.is_empty() {
-                while transform_cursor.start().0 < range.end {
-                    if !transform_cursor.item().unwrap().is_fold() {
-                        let transform_start = self
-                            .buffer_snapshot
-                            .anchor_after(cmp::max(buffer_start, transform_cursor.start().1));
-
-                        let transform_end = {
-                            let overshoot = range.end.0 - transform_cursor.start().0 .0;
-                            self.buffer_snapshot.anchor_before(cmp::min(
-                                transform_cursor.end(&()).1,
-                                transform_cursor.start().1 + overshoot,
-                            ))
-                        };
-
-                        for (tag, highlights) in text_highlights.iter() {
-                            let style = highlights.0;
-                            let ranges = &highlights.1;
-
-                            let start_ix = match ranges.binary_search_by(|probe| {
-                                let cmp = probe.end.cmp(&transform_start, self.buffer_snapshot());
-                                if cmp.is_gt() {
-                                    Ordering::Greater
-                                } else {
-                                    Ordering::Less
-                                }
-                            }) {
-                                Ok(i) | Err(i) => i,
-                            };
-                            for range in &ranges[start_ix..] {
-                                if range
-                                    .start
-                                    .cmp(&transform_end, &self.buffer_snapshot)
-                                    .is_ge()
-                                {
-                                    break;
-                                }
-
-                                highlight_endpoints.push(HighlightEndpoint {
-                                    offset: range.start.to_offset(&self.buffer_snapshot),
-                                    is_start: true,
-                                    tag: *tag,
-                                    style,
-                                });
-                                highlight_endpoints.push(HighlightEndpoint {
-                                    offset: range.end.to_offset(&self.buffer_snapshot),
-                                    is_start: false,
-                                    tag: *tag,
-                                    style,
-                                });
-                            }
-                        }
-                    }
-
-                    transform_cursor.next(&());
-                }
-                highlight_endpoints.sort();
-                transform_cursor.seek(&range.start, Bias::Right, &());
-            }
-        }
-
         FoldChunks {
             transform_cursor,
-            buffer_chunks: self
-                .buffer_snapshot
-                .chunks(buffer_start..buffer_end, language_aware),
-            buffer_chunk: None,
-            buffer_offset: buffer_start,
+            inlay_chunks: self.inlay_snapshot.chunks(
+                inlay_start..inlay_end,
+                language_aware,
+                text_highlights,
+                hint_highlights,
+                suggestion_highlights,
+            ),
+            inlay_chunk: None,
+            inlay_offset: inlay_start,
             output_offset: range.start.0,
             max_output_offset: range.end.0,
-            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
-            active_highlights: Default::default(),
             ellipses_color: self.ellipses_color,
         }
     }
 
+    pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+        self.chunks(start.to_offset(self)..self.len(), false, None, None, None)
+            .flat_map(|chunk| chunk.text.chars())
+    }
+
     #[cfg(test)]
     pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
-        let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>();
-        cursor.seek(&offset, Bias::Right, &());
-        if let Some(transform) = cursor.item() {
-            let transform_start = cursor.start().0 .0;
-            if transform.output_text.is_some() {
-                if offset.0 == transform_start || matches!(bias, Bias::Left) {
-                    FoldOffset(transform_start)
-                } else {
-                    FoldOffset(cursor.end(&()).0 .0)
-                }
-            } else {
-                let overshoot = offset.0 - transform_start;
-                let buffer_offset = cursor.start().1 + overshoot;
-                let clipped_buffer_offset = self.buffer_snapshot.clip_offset(buffer_offset, bias);
-                FoldOffset(
-                    (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize))
-                        as usize,
-                )
-            }
+        if offset > self.len() {
+            self.len()
         } else {
-            FoldOffset(self.transforms.summary().output.len)
+            self.clip_point(offset.to_point(self), bias).to_offset(self)
         }
     }
 
     pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
-        let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>();
+        let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
         cursor.seek(&point, Bias::Right, &());
         if let Some(transform) = cursor.item() {
             let transform_start = cursor.start().0 .0;
@@ -787,11 +712,10 @@ impl FoldSnapshot {
                     FoldPoint(cursor.end(&()).0 .0)
                 }
             } else {
-                let overshoot = point.0 - transform_start;
-                let buffer_position = cursor.start().1 + overshoot;
-                let clipped_buffer_position =
-                    self.buffer_snapshot.clip_point(buffer_position, bias);
-                FoldPoint(cursor.start().0 .0 + (clipped_buffer_position - cursor.start().1))
+                let overshoot = InlayPoint(point.0 - transform_start);
+                let inlay_point = cursor.start().1 + overshoot;
+                let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
+                FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0)
             }
         } else {
             FoldPoint(self.transforms.summary().output.lines)
@@ -800,7 +724,7 @@ impl FoldSnapshot {
 }
 
 fn intersecting_folds<'a, T>(
-    buffer: &'a MultiBufferSnapshot,
+    inlay_snapshot: &'a InlaySnapshot,
     folds: &'a SumTree<Fold>,
     range: Range<T>,
     inclusive: bool,
@@ -808,6 +732,7 @@ fn intersecting_folds<'a, T>(
 where
     T: ToOffset,
 {
+    let buffer = &inlay_snapshot.buffer;
     let start = buffer.anchor_before(range.start.to_offset(buffer));
     let end = buffer.anchor_after(range.end.to_offset(buffer));
     let mut cursor = folds.filter::<_, usize>(move |summary| {
@@ -824,7 +749,7 @@ where
     cursor
 }
 
-fn consolidate_buffer_edits(edits: &mut Vec<text::Edit<usize>>) {
+fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
     edits.sort_unstable_by(|a, b| {
         a.old
             .start
@@ -952,7 +877,7 @@ impl Default for FoldSummary {
 impl sum_tree::Summary for FoldSummary {
     type Context = MultiBufferSnapshot;
 
-    fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) {
+    fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
         if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
             self.min_start = other.min_start.clone();
         }
@@ -996,8 +921,8 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
 
 #[derive(Clone)]
 pub struct FoldBufferRows<'a> {
-    cursor: Cursor<'a, Transform, (FoldPoint, Point)>,
-    input_buffer_rows: MultiBufferRows<'a>,
+    cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
+    input_buffer_rows: InlayBufferRows<'a>,
     fold_point: FoldPoint,
 }
 
@@ -1016,7 +941,7 @@ impl<'a> Iterator for FoldBufferRows<'a> {
 
         if self.cursor.item().is_some() {
             if traversed_fold {
-                self.input_buffer_rows.seek(self.cursor.start().1.row);
+                self.input_buffer_rows.seek(self.cursor.start().1.row());
                 self.input_buffer_rows.next();
             }
             *self.fold_point.row_mut() += 1;
@@ -1028,14 +953,12 @@ impl<'a> Iterator for FoldBufferRows<'a> {
 }
 
 pub struct FoldChunks<'a> {
-    transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>,
-    buffer_chunks: MultiBufferChunks<'a>,
-    buffer_chunk: Option<(usize, Chunk<'a>)>,
-    buffer_offset: usize,
+    transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
+    inlay_chunks: InlayChunks<'a>,
+    inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
+    inlay_offset: InlayOffset,
     output_offset: usize,
     max_output_offset: usize,
-    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
-    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
     ellipses_color: Option<Color>,
 }
 
@@ -1052,11 +975,11 @@ impl<'a> Iterator for FoldChunks<'a> {
         // If we're in a fold, then return the fold's display text and
         // advance the transform and buffer cursors to the end of the fold.
         if let Some(output_text) = transform.output_text {
-            self.buffer_chunk.take();
-            self.buffer_offset += transform.summary.input.len;
-            self.buffer_chunks.seek(self.buffer_offset);
+            self.inlay_chunk.take();
+            self.inlay_offset += InlayOffset(transform.summary.input.len);
+            self.inlay_chunks.seek(self.inlay_offset);
 
-            while self.buffer_offset >= self.transform_cursor.end(&()).1
+            while self.inlay_offset >= self.transform_cursor.end(&()).1
                 && self.transform_cursor.item().is_some()
             {
                 self.transform_cursor.next(&());
@@ -1073,53 +996,28 @@ impl<'a> Iterator for FoldChunks<'a> {
             });
         }
 
-        let mut next_highlight_endpoint = usize::MAX;
-        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
-            if endpoint.offset <= self.buffer_offset {
-                if endpoint.is_start {
-                    self.active_highlights.insert(endpoint.tag, endpoint.style);
-                } else {
-                    self.active_highlights.remove(&endpoint.tag);
-                }
-                self.highlight_endpoints.next();
-            } else {
-                next_highlight_endpoint = endpoint.offset;
-                break;
-            }
-        }
-
         // Retrieve a chunk from the current location in the buffer.
-        if self.buffer_chunk.is_none() {
-            let chunk_offset = self.buffer_chunks.offset();
-            self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk));
+        if self.inlay_chunk.is_none() {
+            let chunk_offset = self.inlay_chunks.offset();
+            self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk));
         }
 
         // Otherwise, take a chunk from the buffer's text.
-        if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk {
-            let buffer_chunk_end = buffer_chunk_start + chunk.text.len();
+        if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+            let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
             let transform_end = self.transform_cursor.end(&()).1;
-            let chunk_end = buffer_chunk_end
-                .min(transform_end)
-                .min(next_highlight_endpoint);
+            let chunk_end = buffer_chunk_end.min(transform_end);
 
             chunk.text = &chunk.text
-                [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start];
-
-            if !self.active_highlights.is_empty() {
-                let mut highlight_style = HighlightStyle::default();
-                for active_highlight in self.active_highlights.values() {
-                    highlight_style.highlight(*active_highlight);
-                }
-                chunk.highlight_style = Some(highlight_style);
-            }
+                [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
 
             if chunk_end == transform_end {
                 self.transform_cursor.next(&());
             } else if chunk_end == buffer_chunk_end {
-                self.buffer_chunk.take();
+                self.inlay_chunk.take();
             }
 
-            self.buffer_offset = chunk_end;
+            self.inlay_offset = chunk_end;
             self.output_offset += chunk.text.len();
             return Some(chunk);
         }

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

@@ -0,0 +1,1697 @@
+use crate::{
+    multi_buffer::{MultiBufferChunks, MultiBufferRows},
+    Anchor, InlayId, MultiBufferSnapshot, ToOffset,
+};
+use collections::{BTreeMap, BTreeSet, HashMap};
+use gpui::fonts::HighlightStyle;
+use language::{Chunk, Edit, Point, Rope, TextSummary};
+use std::{
+    any::TypeId,
+    cmp,
+    iter::Peekable,
+    ops::{Add, AddAssign, Range, Sub, SubAssign},
+    vec,
+};
+use sum_tree::{Bias, Cursor, SumTree};
+use text::Patch;
+
+use super::TextHighlights;
+
+pub struct InlayMap {
+    snapshot: InlaySnapshot,
+    inlays_by_id: HashMap<InlayId, Inlay>,
+    inlays: Vec<Inlay>,
+}
+
+#[derive(Clone)]
+pub struct InlaySnapshot {
+    pub buffer: MultiBufferSnapshot,
+    transforms: SumTree<Transform>,
+    pub version: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Transform {
+    Isomorphic(TextSummary),
+    Inlay(Inlay),
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+    pub id: InlayId,
+    pub position: Anchor,
+    pub text: text::Rope,
+}
+
+#[derive(Debug, Clone)]
+pub struct InlayProperties<T> {
+    pub position: Anchor,
+    pub text: T,
+}
+
+impl sum_tree::Item for Transform {
+    type Summary = TransformSummary;
+
+    fn summary(&self) -> Self::Summary {
+        match self {
+            Transform::Isomorphic(summary) => TransformSummary {
+                input: summary.clone(),
+                output: summary.clone(),
+            },
+            Transform::Inlay(inlay) => TransformSummary {
+                input: TextSummary::default(),
+                output: inlay.text.summary(),
+            },
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+    input: TextSummary,
+    output: TextSummary,
+}
+
+impl sum_tree::Summary for TransformSummary {
+    type Context = ();
+
+    fn add_summary(&mut self, other: &Self, _: &()) {
+        self.input += &other.input;
+        self.output += &other.output;
+    }
+}
+
+pub type InlayEdit = Edit<InlayOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayOffset(pub usize);
+
+impl Add for InlayOffset {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for InlayOffset {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl AddAssign for InlayOffset {
+    fn add_assign(&mut self, rhs: Self) {
+        self.0 += rhs.0;
+    }
+}
+
+impl SubAssign for InlayOffset {
+    fn sub_assign(&mut self, rhs: Self) {
+        self.0 -= rhs.0;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.len;
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayPoint(pub Point);
+
+impl Add for InlayPoint {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+impl Sub for InlayPoint {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self::Output {
+        Self(self.0 - rhs.0)
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        self.0 += &summary.output.lines;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += &summary.input.len;
+    }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
+    fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+        *self += &summary.input.lines;
+    }
+}
+
+#[derive(Clone)]
+pub struct InlayBufferRows<'a> {
+    transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
+    buffer_rows: MultiBufferRows<'a>,
+    inlay_row: u32,
+    max_buffer_row: u32,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+    offset: InlayOffset,
+    is_start: bool,
+    tag: Option<TypeId>,
+    style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for HighlightEndpoint {
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        self.offset
+            .cmp(&other.offset)
+            .then_with(|| other.is_start.cmp(&self.is_start))
+    }
+}
+
+pub struct InlayChunks<'a> {
+    transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
+    buffer_chunks: MultiBufferChunks<'a>,
+    buffer_chunk: Option<Chunk<'a>>,
+    inlay_chunks: Option<text::Chunks<'a>>,
+    output_offset: InlayOffset,
+    max_output_offset: InlayOffset,
+    hint_highlight_style: Option<HighlightStyle>,
+    suggestion_highlight_style: Option<HighlightStyle>,
+    highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+    active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+    snapshot: &'a InlaySnapshot,
+}
+
+impl<'a> InlayChunks<'a> {
+    pub fn seek(&mut self, offset: InlayOffset) {
+        self.transforms.seek(&offset, Bias::Right, &());
+
+        let buffer_offset = self.snapshot.to_buffer_offset(offset);
+        self.buffer_chunks.seek(buffer_offset);
+        self.inlay_chunks = None;
+        self.buffer_chunk = None;
+        self.output_offset = offset;
+    }
+
+    pub fn offset(&self) -> InlayOffset {
+        self.output_offset
+    }
+}
+
+impl<'a> Iterator for InlayChunks<'a> {
+    type Item = Chunk<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.output_offset == self.max_output_offset {
+            return None;
+        }
+
+        let mut next_highlight_endpoint = InlayOffset(usize::MAX);
+        while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+            if endpoint.offset <= self.output_offset {
+                if endpoint.is_start {
+                    self.active_highlights.insert(endpoint.tag, endpoint.style);
+                } else {
+                    self.active_highlights.remove(&endpoint.tag);
+                }
+                self.highlight_endpoints.next();
+            } else {
+                next_highlight_endpoint = endpoint.offset;
+                break;
+            }
+        }
+
+        let chunk = match self.transforms.item()? {
+            Transform::Isomorphic(_) => {
+                let chunk = self
+                    .buffer_chunk
+                    .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+                if chunk.text.is_empty() {
+                    *chunk = self.buffer_chunks.next().unwrap();
+                }
+
+                let (prefix, suffix) = chunk.text.split_at(
+                    chunk
+                        .text
+                        .len()
+                        .min(self.transforms.end(&()).0 .0 - self.output_offset.0)
+                        .min(next_highlight_endpoint.0 - self.output_offset.0),
+                );
+
+                chunk.text = suffix;
+                self.output_offset.0 += prefix.len();
+                let mut prefix = Chunk {
+                    text: prefix,
+                    ..chunk.clone()
+                };
+                if !self.active_highlights.is_empty() {
+                    let mut highlight_style = HighlightStyle::default();
+                    for active_highlight in self.active_highlights.values() {
+                        highlight_style.highlight(*active_highlight);
+                    }
+                    prefix.highlight_style = Some(highlight_style);
+                }
+                prefix
+            }
+            Transform::Inlay(inlay) => {
+                let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
+                    let start = self.output_offset - self.transforms.start().0;
+                    let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
+                        - self.transforms.start().0;
+                    inlay.text.chunks_in_range(start.0..end.0)
+                });
+
+                let chunk = inlay_chunks.next().unwrap();
+                self.output_offset.0 += chunk.len();
+                let highlight_style = match inlay.id {
+                    InlayId::Suggestion(_) => self.suggestion_highlight_style,
+                    InlayId::Hint(_) => self.hint_highlight_style,
+                };
+                Chunk {
+                    text: chunk,
+                    highlight_style,
+                    ..Default::default()
+                }
+            }
+        };
+
+        if self.output_offset == self.transforms.end(&()).0 {
+            self.inlay_chunks = None;
+            self.transforms.next(&());
+        }
+
+        Some(chunk)
+    }
+}
+
+impl<'a> InlayBufferRows<'a> {
+    pub fn seek(&mut self, row: u32) {
+        let inlay_point = InlayPoint::new(row, 0);
+        self.transforms.seek(&inlay_point, Bias::Left, &());
+
+        let mut buffer_point = self.transforms.start().1;
+        let buffer_row = if row == 0 {
+            0
+        } else {
+            match self.transforms.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    buffer_point += inlay_point.0 - self.transforms.start().0 .0;
+                    buffer_point.row
+                }
+                _ => cmp::min(buffer_point.row + 1, self.max_buffer_row),
+            }
+        };
+        self.inlay_row = inlay_point.row();
+        self.buffer_rows.seek(buffer_row);
+    }
+}
+
+impl<'a> Iterator for InlayBufferRows<'a> {
+    type Item = Option<u32>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let buffer_row = if self.inlay_row == 0 {
+            self.buffer_rows.next().unwrap()
+        } else {
+            match self.transforms.item()? {
+                Transform::Inlay(_) => None,
+                Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
+            }
+        };
+
+        self.inlay_row += 1;
+        self.transforms
+            .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
+
+        Some(buffer_row)
+    }
+}
+
+impl InlayPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(Point::new(row, column))
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+}
+
+impl InlayMap {
+    pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
+        let version = 0;
+        let snapshot = InlaySnapshot {
+            buffer: buffer.clone(),
+            transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
+            version,
+        };
+
+        (
+            Self {
+                snapshot: snapshot.clone(),
+                inlays_by_id: HashMap::default(),
+                inlays: Vec::new(),
+            },
+            snapshot,
+        )
+    }
+
+    pub fn sync(
+        &mut self,
+        buffer_snapshot: MultiBufferSnapshot,
+        mut buffer_edits: Vec<text::Edit<usize>>,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        let mut snapshot = &mut self.snapshot;
+
+        if buffer_edits.is_empty() {
+            if snapshot.buffer.trailing_excerpt_update_count()
+                != buffer_snapshot.trailing_excerpt_update_count()
+            {
+                buffer_edits.push(Edit {
+                    old: snapshot.buffer.len()..snapshot.buffer.len(),
+                    new: buffer_snapshot.len()..buffer_snapshot.len(),
+                });
+            }
+        }
+
+        if buffer_edits.is_empty() {
+            if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
+                || snapshot.buffer.parse_count() != buffer_snapshot.parse_count()
+                || snapshot.buffer.diagnostics_update_count()
+                    != buffer_snapshot.diagnostics_update_count()
+                || snapshot.buffer.git_diff_update_count()
+                    != buffer_snapshot.git_diff_update_count()
+                || snapshot.buffer.trailing_excerpt_update_count()
+                    != buffer_snapshot.trailing_excerpt_update_count()
+            {
+                snapshot.version += 1;
+            }
+
+            snapshot.buffer = buffer_snapshot;
+            (snapshot.clone(), Vec::new())
+        } else {
+            let mut inlay_edits = Patch::default();
+            let mut new_transforms = SumTree::new();
+            let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>();
+            let mut buffer_edits_iter = buffer_edits.iter().peekable();
+            while let Some(buffer_edit) = buffer_edits_iter.next() {
+                new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
+                if let Some(Transform::Isomorphic(transform)) = cursor.item() {
+                    if cursor.end(&()).0 == buffer_edit.old.start {
+                        push_isomorphic(&mut new_transforms, transform.clone());
+                        cursor.next(&());
+                    }
+                }
+
+                // Remove all the inlays and transforms contained by the edit.
+                let old_start =
+                    cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
+                cursor.seek(&buffer_edit.old.end, Bias::Right, &());
+                let old_end =
+                    cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
+
+                // Push the unchanged prefix.
+                let prefix_start = new_transforms.summary().input.len;
+                let prefix_end = buffer_edit.new.start;
+                push_isomorphic(
+                    &mut new_transforms,
+                    buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+                );
+                let new_start = InlayOffset(new_transforms.summary().output.len);
+
+                let start_ix = match self.inlays.binary_search_by(|probe| {
+                    probe
+                        .position
+                        .to_offset(&buffer_snapshot)
+                        .cmp(&buffer_edit.new.start)
+                        .then(std::cmp::Ordering::Greater)
+                }) {
+                    Ok(ix) | Err(ix) => ix,
+                };
+
+                for inlay in &self.inlays[start_ix..] {
+                    let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
+                    if buffer_offset > buffer_edit.new.end {
+                        break;
+                    }
+
+                    let prefix_start = new_transforms.summary().input.len;
+                    let prefix_end = buffer_offset;
+                    push_isomorphic(
+                        &mut new_transforms,
+                        buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+                    );
+
+                    if inlay.position.is_valid(&buffer_snapshot) {
+                        new_transforms.push(Transform::Inlay(inlay.clone()), &());
+                    }
+                }
+
+                // Apply the rest of the edit.
+                let transform_start = new_transforms.summary().input.len;
+                push_isomorphic(
+                    &mut new_transforms,
+                    buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
+                );
+                let new_end = InlayOffset(new_transforms.summary().output.len);
+                inlay_edits.push(Edit {
+                    old: old_start..old_end,
+                    new: new_start..new_end,
+                });
+
+                // If the next edit doesn't intersect the current isomorphic transform, then
+                // we can push its remainder.
+                if buffer_edits_iter
+                    .peek()
+                    .map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
+                {
+                    let transform_start = new_transforms.summary().input.len;
+                    let transform_end =
+                        buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
+                    push_isomorphic(
+                        &mut new_transforms,
+                        buffer_snapshot.text_summary_for_range(transform_start..transform_end),
+                    );
+                    cursor.next(&());
+                }
+            }
+
+            new_transforms.append(cursor.suffix(&()), &());
+            if new_transforms.is_empty() {
+                new_transforms.push(Transform::Isomorphic(Default::default()), &());
+            }
+
+            drop(cursor);
+            snapshot.transforms = new_transforms;
+            snapshot.version += 1;
+            snapshot.buffer = buffer_snapshot;
+            snapshot.check_invariants();
+
+            (snapshot.clone(), inlay_edits.into_inner())
+        }
+    }
+
+    pub fn splice<T: Into<Rope>>(
+        &mut self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<(InlayId, InlayProperties<T>)>,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        let snapshot = &mut self.snapshot;
+        let mut edits = BTreeSet::new();
+
+        self.inlays.retain(|inlay| !to_remove.contains(&inlay.id));
+        for inlay_id in to_remove {
+            if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) {
+                let offset = inlay.position.to_offset(&snapshot.buffer);
+                edits.insert(offset);
+            }
+        }
+
+        for (existing_id, properties) in to_insert {
+            let inlay = Inlay {
+                id: existing_id,
+                position: properties.position,
+                text: properties.text.into(),
+            };
+
+            // Avoid inserting empty inlays.
+            if inlay.text.is_empty() {
+                continue;
+            }
+
+            self.inlays_by_id.insert(inlay.id, inlay.clone());
+            match self
+                .inlays
+                .binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer))
+            {
+                Ok(ix) | Err(ix) => {
+                    self.inlays.insert(ix, inlay.clone());
+                }
+            }
+
+            let offset = inlay.position.to_offset(&snapshot.buffer);
+            edits.insert(offset);
+        }
+
+        let buffer_edits = edits
+            .into_iter()
+            .map(|offset| Edit {
+                old: offset..offset,
+                new: offset..offset,
+            })
+            .collect();
+        let buffer_snapshot = snapshot.buffer.clone();
+        drop(snapshot);
+        let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
+        (snapshot, edits)
+    }
+
+    pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+        self.inlays.iter()
+    }
+
+    #[cfg(test)]
+    pub(crate) fn randomly_mutate(
+        &mut self,
+        next_inlay_id: &mut usize,
+        rng: &mut rand::rngs::StdRng,
+    ) -> (InlaySnapshot, Vec<InlayEdit>) {
+        use rand::prelude::*;
+        use util::post_inc;
+
+        let mut to_remove = Vec::new();
+        let mut to_insert = Vec::new();
+        let snapshot = &mut self.snapshot;
+        for i in 0..rng.gen_range(1..=5) {
+            if self.inlays.is_empty() || rng.gen() {
+                let position = snapshot.buffer.random_byte_range(0, rng).start;
+                let bias = if rng.gen() { Bias::Left } else { Bias::Right };
+                let len = if rng.gen_bool(0.01) {
+                    0
+                } else {
+                    rng.gen_range(1..=5)
+                };
+                let text = util::RandomCharIter::new(&mut *rng)
+                    .filter(|ch| *ch != '\r')
+                    .take(len)
+                    .collect::<String>();
+                log::info!(
+                    "creating inlay at buffer offset {} with bias {:?} and text {:?}",
+                    position,
+                    bias,
+                    text
+                );
+
+                let inlay_id = if i % 2 == 0 {
+                    InlayId::Hint(post_inc(next_inlay_id))
+                } else {
+                    InlayId::Suggestion(post_inc(next_inlay_id))
+                };
+                to_insert.push((
+                    inlay_id,
+                    InlayProperties {
+                        position: snapshot.buffer.anchor_at(position, bias),
+                        text,
+                    },
+                ));
+            } else {
+                to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap());
+            }
+        }
+        log::info!("removing inlays: {:?}", to_remove);
+
+        drop(snapshot);
+        let (snapshot, edits) = self.splice(to_remove, to_insert);
+        (snapshot, edits)
+    }
+}
+
+impl InlaySnapshot {
+    pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
+        let mut cursor = self
+            .transforms
+            .cursor::<(InlayOffset, (InlayPoint, usize))>();
+        cursor.seek(&offset, Bias::Right, &());
+        let overshoot = offset.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_offset_start = cursor.start().1 .1;
+                let buffer_offset_end = buffer_offset_start + overshoot;
+                let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
+                let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
+                InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let overshoot = inlay.text.offset_to_point(overshoot);
+                InlayPoint(cursor.start().1 .0 .0 + overshoot)
+            }
+            None => self.max_point(),
+        }
+    }
+
+    pub fn len(&self) -> InlayOffset {
+        InlayOffset(self.transforms.summary().output.len)
+    }
+
+    pub fn max_point(&self) -> InlayPoint {
+        InlayPoint(self.transforms.summary().output.lines)
+    }
+
+    pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
+        let mut cursor = self
+            .transforms
+            .cursor::<(InlayPoint, (InlayOffset, Point))>();
+        cursor.seek(&point, Bias::Right, &());
+        let overshoot = point.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_point_start = cursor.start().1 .1;
+                let buffer_point_end = buffer_point_start + overshoot;
+                let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
+                let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
+                InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let overshoot = inlay.text.point_to_offset(overshoot);
+                InlayOffset(cursor.start().1 .0 .0 + overshoot)
+            }
+            None => self.len(),
+        }
+    }
+
+    pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        cursor.seek(&point, Bias::Right, &());
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let overshoot = point.0 - cursor.start().0 .0;
+                cursor.start().1 + overshoot
+            }
+            Some(Transform::Inlay(_)) => cursor.start().1,
+            None => self.buffer.max_point(),
+        }
+    }
+
+    pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&offset, Bias::Right, &());
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let overshoot = offset - cursor.start().0;
+                cursor.start().1 + overshoot.0
+            }
+            Some(Transform::Inlay(_)) => cursor.start().1,
+            None => self.buffer.len(),
+        }
+    }
+
+    pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
+        let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>();
+        cursor.seek(&offset, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    if offset == cursor.end(&()).0 {
+                        while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                break;
+                            } else {
+                                cursor.next(&());
+                            }
+                        }
+                        return cursor.end(&()).1;
+                    } else {
+                        let overshoot = offset - cursor.start().0;
+                        return InlayOffset(cursor.start().1 .0 + overshoot);
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if inlay.position.bias() == Bias::Left {
+                        cursor.next(&());
+                    } else {
+                        return cursor.start().1;
+                    }
+                }
+                None => {
+                    return self.len();
+                }
+            }
+        }
+    }
+
+    pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
+        let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>();
+        cursor.seek(&point, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    if point == cursor.end(&()).0 {
+                        while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                break;
+                            } else {
+                                cursor.next(&());
+                            }
+                        }
+                        return cursor.end(&()).1;
+                    } else {
+                        let overshoot = point - cursor.start().0;
+                        return InlayPoint(cursor.start().1 .0 + overshoot);
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if inlay.position.bias() == Bias::Left {
+                        cursor.next(&());
+                    } else {
+                        return cursor.start().1;
+                    }
+                }
+                None => {
+                    return self.max_point();
+                }
+            }
+        }
+    }
+
+    pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        cursor.seek(&point, Bias::Left, &());
+        loop {
+            match cursor.item() {
+                Some(Transform::Isomorphic(transform)) => {
+                    if cursor.start().0 == point {
+                        if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
+                            if inlay.position.bias() == Bias::Left {
+                                return point;
+                            } else if bias == Bias::Left {
+                                cursor.prev(&());
+                            } else if transform.first_line_chars == 0 {
+                                point.0 += Point::new(1, 0);
+                            } else {
+                                point.0 += Point::new(0, 1);
+                            }
+                        } else {
+                            return point;
+                        }
+                    } else if cursor.end(&()).0 == point {
+                        if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+                            if inlay.position.bias() == Bias::Right {
+                                return point;
+                            } else if bias == Bias::Right {
+                                cursor.next(&());
+                            } else if point.0.column == 0 {
+                                point.0.row -= 1;
+                                point.0.column = self.line_len(point.0.row);
+                            } else {
+                                point.0.column -= 1;
+                            }
+                        } else {
+                            return point;
+                        }
+                    } else {
+                        let overshoot = point.0 - cursor.start().0 .0;
+                        let buffer_point = cursor.start().1 + overshoot;
+                        let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
+                        let clipped_overshoot = clipped_buffer_point - cursor.start().1;
+                        let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
+                        if clipped_point == point {
+                            return clipped_point;
+                        } else {
+                            point = clipped_point;
+                        }
+                    }
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
+                        match cursor.prev_item() {
+                            Some(Transform::Inlay(inlay)) => {
+                                if inlay.position.bias() == Bias::Left {
+                                    return point;
+                                }
+                            }
+                            _ => return point,
+                        }
+                    } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
+                        match cursor.next_item() {
+                            Some(Transform::Inlay(inlay)) => {
+                                if inlay.position.bias() == Bias::Right {
+                                    return point;
+                                }
+                            }
+                            _ => return point,
+                        }
+                    }
+
+                    if bias == Bias::Left {
+                        point = cursor.start().0;
+                        cursor.prev(&());
+                    } else {
+                        cursor.next(&());
+                        point = cursor.start().0;
+                    }
+                }
+                None => {
+                    bias = bias.invert();
+                    if bias == Bias::Left {
+                        point = cursor.start().0;
+                        cursor.prev(&());
+                    } else {
+                        cursor.next(&());
+                        point = cursor.start().0;
+                    }
+                }
+            }
+        }
+    }
+
+    pub fn text_summary(&self) -> TextSummary {
+        self.transforms.summary().output.clone()
+    }
+
+    pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
+        let mut summary = TextSummary::default();
+
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&range.start, Bias::Right, &());
+
+        let overshoot = range.start.0 - cursor.start().0 .0;
+        match cursor.item() {
+            Some(Transform::Isomorphic(_)) => {
+                let buffer_start = cursor.start().1;
+                let suffix_start = buffer_start + overshoot;
+                let suffix_end =
+                    buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
+                summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
+                cursor.next(&());
+            }
+            Some(Transform::Inlay(inlay)) => {
+                let suffix_start = overshoot;
+                let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0;
+                summary = inlay.text.cursor(suffix_start).summary(suffix_end);
+                cursor.next(&());
+            }
+            None => {}
+        }
+
+        if range.end > cursor.start().0 {
+            summary += cursor
+                .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+                .output;
+
+            let overshoot = range.end.0 - cursor.start().0 .0;
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    let prefix_start = cursor.start().1;
+                    let prefix_end = prefix_start + overshoot;
+                    summary += self
+                        .buffer
+                        .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
+                }
+                Some(Transform::Inlay(inlay)) => {
+                    let prefix_end = overshoot;
+                    summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
+                }
+                None => {}
+            }
+        }
+
+        summary
+    }
+
+    pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> {
+        let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+        let inlay_point = InlayPoint::new(row, 0);
+        cursor.seek(&inlay_point, Bias::Left, &());
+
+        let max_buffer_row = self.buffer.max_point().row;
+        let mut buffer_point = cursor.start().1;
+        let buffer_row = if row == 0 {
+            0
+        } else {
+            match cursor.item() {
+                Some(Transform::Isomorphic(_)) => {
+                    buffer_point += inlay_point.0 - cursor.start().0 .0;
+                    buffer_point.row
+                }
+                _ => cmp::min(buffer_point.row + 1, max_buffer_row),
+            }
+        };
+
+        InlayBufferRows {
+            transforms: cursor,
+            inlay_row: inlay_point.row(),
+            buffer_rows: self.buffer.buffer_rows(buffer_row),
+            max_buffer_row,
+        }
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
+        let line_end = if row >= self.max_point().row() {
+            self.len().0
+        } else {
+            self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
+        };
+        (line_end - line_start) as u32
+    }
+
+    pub fn chunks<'a>(
+        &'a self,
+        range: Range<InlayOffset>,
+        language_aware: bool,
+        text_highlights: Option<&'a TextHighlights>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
+    ) -> InlayChunks<'a> {
+        let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+        cursor.seek(&range.start, Bias::Right, &());
+
+        let mut highlight_endpoints = Vec::new();
+        if let Some(text_highlights) = text_highlights {
+            if !text_highlights.is_empty() {
+                while cursor.start().0 < range.end {
+                    if true {
+                        let transform_start = self.buffer.anchor_after(
+                            self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
+                        );
+
+                        let transform_end = {
+                            let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+                            self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+                                cursor.end(&()).0,
+                                cursor.start().0 + overshoot,
+                            )))
+                        };
+
+                        for (tag, highlights) in text_highlights.iter() {
+                            let style = highlights.0;
+                            let ranges = &highlights.1;
+
+                            let start_ix = match ranges.binary_search_by(|probe| {
+                                let cmp = probe.end.cmp(&transform_start, &self.buffer);
+                                if cmp.is_gt() {
+                                    cmp::Ordering::Greater
+                                } else {
+                                    cmp::Ordering::Less
+                                }
+                            }) {
+                                Ok(i) | Err(i) => i,
+                            };
+                            for range in &ranges[start_ix..] {
+                                if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+                                    break;
+                                }
+
+                                highlight_endpoints.push(HighlightEndpoint {
+                                    offset: self
+                                        .to_inlay_offset(range.start.to_offset(&self.buffer)),
+                                    is_start: true,
+                                    tag: *tag,
+                                    style,
+                                });
+                                highlight_endpoints.push(HighlightEndpoint {
+                                    offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+                                    is_start: false,
+                                    tag: *tag,
+                                    style,
+                                });
+                            }
+                        }
+                    }
+
+                    cursor.next(&());
+                }
+                highlight_endpoints.sort();
+                cursor.seek(&range.start, Bias::Right, &());
+            }
+        }
+
+        let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
+        let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
+
+        InlayChunks {
+            transforms: cursor,
+            buffer_chunks,
+            inlay_chunks: None,
+            buffer_chunk: None,
+            output_offset: range.start,
+            max_output_offset: range.end,
+            hint_highlight_style: hint_highlights,
+            suggestion_highlight_style: suggestion_highlights,
+            highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+            active_highlights: Default::default(),
+            snapshot: self,
+        }
+    }
+
+    #[cfg(test)]
+    pub fn text(&self) -> String {
+        self.chunks(Default::default()..self.len(), false, None, None, None)
+            .map(|chunk| chunk.text)
+            .collect()
+    }
+
+    fn check_invariants(&self) {
+        #[cfg(any(debug_assertions, feature = "test-support"))]
+        {
+            assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
+            let mut transforms = self.transforms.iter().peekable();
+            while let Some(transform) = transforms.next() {
+                let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
+                if let Some(next_transform) = transforms.peek() {
+                    let next_transform_is_isomorphic =
+                        matches!(next_transform, Transform::Isomorphic(_));
+                    assert!(
+                        !transform_is_isomorphic || !next_transform_is_isomorphic,
+                        "two adjacent isomorphic transforms"
+                    );
+                }
+            }
+        }
+    }
+}
+
+fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
+    if summary.len == 0 {
+        return;
+    }
+
+    let mut summary = Some(summary);
+    sum_tree.update_last(
+        |transform| {
+            if let Transform::Isomorphic(transform) = transform {
+                *transform += summary.take().unwrap();
+            }
+        },
+        &(),
+    );
+
+    if let Some(summary) = summary {
+        sum_tree.push(Transform::Isomorphic(summary), &());
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{InlayId, MultiBuffer};
+    use gpui::AppContext;
+    use rand::prelude::*;
+    use settings::SettingsStore;
+    use std::{cmp::Reverse, env, sync::Arc};
+    use sum_tree::TreeMap;
+    use text::Patch;
+    use util::post_inc;
+
+    #[gpui::test]
+    fn test_basic_inlays(cx: &mut AppContext) {
+        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        assert_eq!(inlay_snapshot.text(), "abcdefghi");
+        let mut next_inlay_id = 0;
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![(
+                InlayId::Hint(post_inc(&mut next_inlay_id)),
+                InlayProperties {
+                    position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                    text: "|123|",
+                },
+            )],
+        );
+        assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 0)),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 1)),
+            InlayPoint::new(0, 1)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 2)),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 3)),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 4)),
+            InlayPoint::new(0, 9)
+        );
+        assert_eq!(
+            inlay_snapshot.to_inlay_point(Point::new(0, 5)),
+            InlayPoint::new(0, 10)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+            InlayPoint::new(0, 3)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+            InlayPoint::new(0, 9)
+        );
+
+        // Edits before or after the inlay should not affect it.
+        buffer.update(cx, |buffer, cx| {
+            buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
+        });
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
+
+        // An edit surrounding the inlay should invalidate it.
+        buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![
+                (
+                    InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    InlayProperties {
+                        position: buffer.read(cx).snapshot(cx).anchor_before(3),
+                        text: "|123|",
+                    },
+                ),
+                (
+                    InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    InlayProperties {
+                        position: buffer.read(cx).snapshot(cx).anchor_after(3),
+                        text: "|456|",
+                    },
+                ),
+            ],
+        );
+        assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
+
+        // Edits ending where the inlay starts should not move it if it has a left bias.
+        buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
+        let (inlay_snapshot, _) = inlay_map.sync(
+            buffer.read(cx).snapshot(cx),
+            buffer_edits.consume().into_inner(),
+        );
+        assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+            InlayPoint::new(0, 0)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+            InlayPoint::new(0, 0)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
+            InlayPoint::new(0, 1)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
+            InlayPoint::new(0, 1)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
+            InlayPoint::new(0, 2)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
+            InlayPoint::new(0, 2)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
+            InlayPoint::new(0, 8)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
+            InlayPoint::new(0, 8)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
+            InlayPoint::new(0, 9)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
+            InlayPoint::new(0, 9)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
+            InlayPoint::new(0, 10)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
+            InlayPoint::new(0, 10)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
+            InlayPoint::new(0, 11)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
+            InlayPoint::new(0, 11)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
+            InlayPoint::new(0, 17)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
+            InlayPoint::new(0, 17)
+        );
+
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
+            InlayPoint::new(0, 18)
+        );
+        assert_eq!(
+            inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
+            InlayPoint::new(0, 18)
+        );
+
+        // The inlays can be manually removed.
+        let (inlay_snapshot, _) = inlay_map
+            .splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new());
+        assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
+    }
+
+    #[gpui::test]
+    fn test_inlay_buffer_rows(cx: &mut AppContext) {
+        let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+        assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
+        let mut next_inlay_id = 0;
+
+        let (inlay_snapshot, _) = inlay_map.splice(
+            Vec::new(),
+            vec![
+                (
+                    InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    InlayProperties {
+                        position: buffer.read(cx).snapshot(cx).anchor_before(0),
+                        text: "|123|\n",
+                    },
+                ),
+                (
+                    InlayId::Hint(post_inc(&mut next_inlay_id)),
+                    InlayProperties {
+                        position: buffer.read(cx).snapshot(cx).anchor_before(4),
+                        text: "|456|",
+                    },
+                ),
+                (
+                    InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+                    InlayProperties {
+                        position: buffer.read(cx).snapshot(cx).anchor_before(7),
+                        text: "\n|567|\n",
+                    },
+                ),
+            ],
+        );
+        assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
+        assert_eq!(
+            inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+            vec![Some(0), None, Some(1), None, None, Some(2)]
+        );
+    }
+
+    #[gpui::test(iterations = 100)]
+    fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) {
+        init_test(cx);
+
+        let operations = env::var("OPERATIONS")
+            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+            .unwrap_or(10);
+
+        let len = rng.gen_range(0..30);
+        let buffer = if rng.gen() {
+            let text = util::RandomCharIter::new(&mut rng)
+                .take(len)
+                .collect::<String>();
+            MultiBuffer::build_simple(&text, cx)
+        } else {
+            MultiBuffer::build_random(&mut rng, cx)
+        };
+        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let mut next_inlay_id = 0;
+        log::info!("buffer text: {:?}", buffer_snapshot.text());
+
+        let mut highlights = TreeMap::default();
+        let highlight_count = rng.gen_range(0_usize..10);
+        let mut highlight_ranges = (0..highlight_count)
+            .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+            .collect::<Vec<_>>();
+        highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+        log::info!("highlighting ranges {:?}", highlight_ranges);
+        let highlight_ranges = highlight_ranges
+            .into_iter()
+            .map(|range| {
+                buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
+            })
+            .collect::<Vec<_>>();
+
+        highlights.insert(
+            Some(TypeId::of::<()>()),
+            Arc::new((HighlightStyle::default(), highlight_ranges)),
+        );
+
+        let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        for _ in 0..operations {
+            let mut inlay_edits = Patch::default();
+
+            let mut prev_inlay_text = inlay_snapshot.text();
+            let mut buffer_edits = Vec::new();
+            match rng.gen_range(0..=100) {
+                0..=50 => {
+                    let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    log::info!("mutated text: {:?}", snapshot.text());
+                    inlay_edits = Patch::new(edits);
+                }
+                _ => buffer.update(cx, |buffer, cx| {
+                    let subscription = buffer.subscribe();
+                    let edit_count = rng.gen_range(1..=5);
+                    buffer.randomly_mutate(&mut rng, edit_count, cx);
+                    buffer_snapshot = buffer.snapshot(cx);
+                    let edits = subscription.consume().into_inner();
+                    log::info!("editing {:?}", edits);
+                    buffer_edits.extend(edits);
+                }),
+            };
+
+            let (new_inlay_snapshot, new_inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            inlay_snapshot = new_inlay_snapshot;
+            inlay_edits = inlay_edits.compose(new_inlay_edits);
+
+            log::info!("buffer text: {:?}", buffer_snapshot.text());
+            log::info!("inlay text: {:?}", inlay_snapshot.text());
+
+            let inlays = inlay_map
+                .inlays
+                .iter()
+                .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
+                .map(|inlay| {
+                    let offset = inlay.position.to_offset(&buffer_snapshot);
+                    (offset, inlay.clone())
+                })
+                .collect::<Vec<_>>();
+            let mut expected_text = Rope::from(buffer_snapshot.text().as_str());
+            for (offset, inlay) in inlays.into_iter().rev() {
+                expected_text.replace(offset..offset, &inlay.text.to_string());
+            }
+            assert_eq!(inlay_snapshot.text(), expected_text.to_string());
+
+            let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+            assert_eq!(
+                expected_buffer_rows.len() as u32,
+                expected_text.max_point().row + 1
+            );
+            for row_start in 0..expected_buffer_rows.len() {
+                assert_eq!(
+                    inlay_snapshot
+                        .buffer_rows(row_start as u32)
+                        .collect::<Vec<_>>(),
+                    &expected_buffer_rows[row_start..],
+                    "incorrect buffer rows starting at {}",
+                    row_start
+                );
+            }
+
+            for _ in 0..5 {
+                let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
+                end = expected_text.clip_offset(end, Bias::Right);
+                let mut start = rng.gen_range(0..=end);
+                start = expected_text.clip_offset(start, Bias::Right);
+
+                let actual_text = inlay_snapshot
+                    .chunks(
+                        InlayOffset(start)..InlayOffset(end),
+                        false,
+                        Some(&highlights),
+                        None,
+                        None,
+                    )
+                    .map(|chunk| chunk.text)
+                    .collect::<String>();
+                assert_eq!(
+                    actual_text,
+                    expected_text.slice(start..end).to_string(),
+                    "incorrect text in range {:?}",
+                    start..end
+                );
+
+                assert_eq!(
+                    inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
+                    expected_text.slice(start..end).summary()
+                );
+            }
+
+            for edit in inlay_edits {
+                prev_inlay_text.replace_range(
+                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+                    &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
+                );
+            }
+            assert_eq!(prev_inlay_text, inlay_snapshot.text());
+
+            assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
+            assert_eq!(expected_text.len(), inlay_snapshot.len().0);
+
+            let mut buffer_point = Point::default();
+            let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+            let mut buffer_chars = buffer_snapshot.chars_at(0);
+            loop {
+                // Ensure conversion from buffer coordinates to inlay coordinates
+                // is consistent.
+                let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
+                assert_eq!(
+                    inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
+                    inlay_point
+                );
+
+                // No matter which bias we clip an inlay point with, it doesn't move
+                // because it was constructed from a buffer point.
+                assert_eq!(
+                    inlay_snapshot.clip_point(inlay_point, Bias::Left),
+                    inlay_point,
+                    "invalid inlay point for buffer point {:?} when clipped left",
+                    buffer_point
+                );
+                assert_eq!(
+                    inlay_snapshot.clip_point(inlay_point, Bias::Right),
+                    inlay_point,
+                    "invalid inlay point for buffer point {:?} when clipped right",
+                    buffer_point
+                );
+
+                if let Some(ch) = buffer_chars.next() {
+                    if ch == '\n' {
+                        buffer_point += Point::new(1, 0);
+                    } else {
+                        buffer_point += Point::new(0, ch.len_utf8() as u32);
+                    }
+
+                    // Ensure that moving forward in the buffer always moves the inlay point forward as well.
+                    let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+                    assert!(new_inlay_point > inlay_point);
+                    inlay_point = new_inlay_point;
+                } else {
+                    break;
+                }
+            }
+
+            let mut inlay_point = InlayPoint::default();
+            let mut inlay_offset = InlayOffset::default();
+            for ch in expected_text.chars() {
+                assert_eq!(
+                    inlay_snapshot.to_offset(inlay_point),
+                    inlay_offset,
+                    "invalid to_offset({:?})",
+                    inlay_point
+                );
+                assert_eq!(
+                    inlay_snapshot.to_point(inlay_offset),
+                    inlay_point,
+                    "invalid to_point({:?})",
+                    inlay_offset
+                );
+
+                let mut bytes = [0; 4];
+                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+                    inlay_offset.0 += 1;
+                    if *byte == b'\n' {
+                        inlay_point.0 += Point::new(1, 0);
+                    } else {
+                        inlay_point.0 += Point::new(0, 1);
+                    }
+
+                    let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
+                    let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
+                    assert!(
+                        clipped_left_point <= clipped_right_point,
+                        "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
+                        inlay_point,
+                        clipped_left_point,
+                        clipped_right_point
+                    );
+
+                    // Ensure the clipped points are at valid text locations.
+                    assert_eq!(
+                        clipped_left_point.0,
+                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
+                    );
+                    assert_eq!(
+                        clipped_right_point.0,
+                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
+                    );
+
+                    // Ensure the clipped points never overshoot the end of the map.
+                    assert!(clipped_left_point <= inlay_snapshot.max_point());
+                    assert!(clipped_right_point <= inlay_snapshot.max_point());
+
+                    // Ensure the clipped points are at valid buffer locations.
+                    assert_eq!(
+                        inlay_snapshot
+                            .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
+                        clipped_left_point,
+                        "to_buffer_point({:?}) = {:?}",
+                        clipped_left_point,
+                        inlay_snapshot.to_buffer_point(clipped_left_point),
+                    );
+                    assert_eq!(
+                        inlay_snapshot
+                            .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
+                        clipped_right_point,
+                        "to_buffer_point({:?}) = {:?}",
+                        clipped_right_point,
+                        inlay_snapshot.to_buffer_point(clipped_right_point),
+                    );
+                }
+            }
+        }
+    }
+
+    fn init_test(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+    }
+}

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

@@ -1,871 +0,0 @@
-use super::{
-    fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
-    TextHighlights,
-};
-use crate::{MultiBufferSnapshot, ToPoint};
-use gpui::fonts::HighlightStyle;
-use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
-use parking_lot::Mutex;
-use std::{
-    cmp,
-    ops::{Add, AddAssign, Range, Sub},
-};
-use util::post_inc;
-
-pub type SuggestionEdit = Edit<SuggestionOffset>;
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionOffset(pub usize);
-
-impl Add for SuggestionOffset {
-    type Output = Self;
-
-    fn add(self, rhs: Self) -> Self::Output {
-        Self(self.0 + rhs.0)
-    }
-}
-
-impl Sub for SuggestionOffset {
-    type Output = Self;
-
-    fn sub(self, rhs: Self) -> Self::Output {
-        Self(self.0 - rhs.0)
-    }
-}
-
-impl AddAssign for SuggestionOffset {
-    fn add_assign(&mut self, rhs: Self) {
-        self.0 += rhs.0;
-    }
-}
-
-#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct SuggestionPoint(pub Point);
-
-impl SuggestionPoint {
-    pub fn new(row: u32, column: u32) -> Self {
-        Self(Point::new(row, column))
-    }
-
-    pub fn row(self) -> u32 {
-        self.0.row
-    }
-
-    pub fn column(self) -> u32 {
-        self.0.column
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct Suggestion<T> {
-    pub position: T,
-    pub text: Rope,
-}
-
-pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
-
-impl SuggestionMap {
-    pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
-        let snapshot = SuggestionSnapshot {
-            fold_snapshot,
-            suggestion: None,
-            version: 0,
-        };
-        (Self(Mutex::new(snapshot.clone())), snapshot)
-    }
-
-    pub fn replace<T>(
-        &self,
-        new_suggestion: Option<Suggestion<T>>,
-        fold_snapshot: FoldSnapshot,
-        fold_edits: Vec<FoldEdit>,
-    ) -> (
-        SuggestionSnapshot,
-        Vec<SuggestionEdit>,
-        Option<Suggestion<FoldOffset>>,
-    )
-    where
-        T: ToPoint,
-    {
-        let new_suggestion = new_suggestion.map(|new_suggestion| {
-            let buffer_point = new_suggestion
-                .position
-                .to_point(fold_snapshot.buffer_snapshot());
-            let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
-            let fold_offset = fold_point.to_offset(&fold_snapshot);
-            Suggestion {
-                position: fold_offset,
-                text: new_suggestion.text,
-            }
-        });
-
-        let (_, edits) = self.sync(fold_snapshot, fold_edits);
-        let mut snapshot = self.0.lock();
-
-        let mut patch = Patch::new(edits);
-        let old_suggestion = snapshot.suggestion.take();
-        if let Some(suggestion) = &old_suggestion {
-            patch = patch.compose([SuggestionEdit {
-                old: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
-                new: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0),
-            }]);
-        }
-
-        if let Some(suggestion) = new_suggestion.as_ref() {
-            patch = patch.compose([SuggestionEdit {
-                old: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0),
-                new: SuggestionOffset(suggestion.position.0)
-                    ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
-            }]);
-        }
-
-        snapshot.suggestion = new_suggestion;
-        snapshot.version += 1;
-        (snapshot.clone(), patch.into_inner(), old_suggestion)
-    }
-
-    pub fn sync(
-        &self,
-        fold_snapshot: FoldSnapshot,
-        fold_edits: Vec<FoldEdit>,
-    ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
-        let mut snapshot = self.0.lock();
-
-        if snapshot.fold_snapshot.version != fold_snapshot.version {
-            snapshot.version += 1;
-        }
-
-        let mut suggestion_edits = Vec::new();
-
-        let mut suggestion_old_len = 0;
-        let mut suggestion_new_len = 0;
-        for fold_edit in fold_edits {
-            let start = fold_edit.new.start;
-            let end = FoldOffset(start.0 + fold_edit.old_len().0);
-            if let Some(suggestion) = snapshot.suggestion.as_mut() {
-                if end <= suggestion.position {
-                    suggestion.position.0 += fold_edit.new_len().0;
-                    suggestion.position.0 -= fold_edit.old_len().0;
-                } else if start > suggestion.position {
-                    suggestion_old_len = suggestion.text.len();
-                    suggestion_new_len = suggestion_old_len;
-                } else {
-                    suggestion_old_len = suggestion.text.len();
-                    snapshot.suggestion.take();
-                    suggestion_edits.push(SuggestionEdit {
-                        old: SuggestionOffset(fold_edit.old.start.0)
-                            ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
-                        new: SuggestionOffset(fold_edit.new.start.0)
-                            ..SuggestionOffset(fold_edit.new.end.0),
-                    });
-                    continue;
-                }
-            }
-
-            suggestion_edits.push(SuggestionEdit {
-                old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
-                    ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
-                new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
-                    ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
-            });
-        }
-        snapshot.fold_snapshot = fold_snapshot;
-
-        (snapshot.clone(), suggestion_edits)
-    }
-
-    pub fn has_suggestion(&self) -> bool {
-        let snapshot = self.0.lock();
-        snapshot.suggestion.is_some()
-    }
-}
-
-#[derive(Clone)]
-pub struct SuggestionSnapshot {
-    pub fold_snapshot: FoldSnapshot,
-    pub suggestion: Option<Suggestion<FoldOffset>>,
-    pub version: usize,
-}
-
-impl SuggestionSnapshot {
-    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        self.fold_snapshot.buffer_snapshot()
-    }
-
-    pub fn max_point(&self) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
-            let mut max_point = suggestion_point.0;
-            max_point += suggestion.text.max_point();
-            max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
-            SuggestionPoint(max_point)
-        } else {
-            SuggestionPoint(self.fold_snapshot.max_point().0)
-        }
-    }
-
-    pub fn len(&self) -> SuggestionOffset {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let mut len = suggestion.position.0;
-            len += suggestion.text.len();
-            len += self.fold_snapshot.len().0 - suggestion.position.0;
-            SuggestionOffset(len)
-        } else {
-            SuggestionOffset(self.fold_snapshot.len().0)
-        }
-    }
-
-    pub fn line_len(&self, row: u32) -> u32 {
-        if let Some(suggestion) = &self.suggestion {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-
-            if row < suggestion_start.row {
-                self.fold_snapshot.line_len(row)
-            } else if row > suggestion_end.row {
-                self.fold_snapshot
-                    .line_len(suggestion_start.row + (row - suggestion_end.row))
-            } else {
-                let mut result = suggestion.text.line_len(row - suggestion_start.row);
-                if row == suggestion_start.row {
-                    result += suggestion_start.column;
-                }
-                if row == suggestion_end.row {
-                    result +=
-                        self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
-                }
-                result
-            }
-        } else {
-            self.fold_snapshot.line_len(row)
-        }
-    }
-
-    pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-            if point.0 <= suggestion_start {
-                SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
-            } else if point.0 > suggestion_end {
-                let fold_point = self.fold_snapshot.clip_point(
-                    FoldPoint(suggestion_start + (point.0 - suggestion_end)),
-                    bias,
-                );
-                let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
-                if bias == Bias::Left && suggestion_point == suggestion_end {
-                    SuggestionPoint(suggestion_start)
-                } else {
-                    SuggestionPoint(suggestion_point)
-                }
-            } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
-                SuggestionPoint(suggestion_start)
-            } else {
-                let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
-                    > suggestion_start.column
-                {
-                    FoldPoint(suggestion_start + Point::new(0, 1))
-                } else {
-                    FoldPoint(suggestion_start + Point::new(1, 0))
-                };
-                let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
-                SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
-            }
-        } else {
-            SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
-        }
-    }
-
-    pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-
-            if point.0 <= suggestion_start {
-                SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
-            } else if point.0 > suggestion_end {
-                let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
-                    .to_offset(&self.fold_snapshot);
-                SuggestionOffset(fold_offset.0 + suggestion.text.len())
-            } else {
-                let offset_in_suggestion =
-                    suggestion.text.point_to_offset(point.0 - suggestion_start);
-                SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
-            }
-        } else {
-            SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
-        }
-    }
-
-    pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            if offset.0 <= suggestion.position.0 {
-                SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
-            } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
-                let fold_point = FoldOffset(offset.0 - suggestion.text.len())
-                    .to_point(&self.fold_snapshot)
-                    .0;
-
-                SuggestionPoint(
-                    suggestion_point_start
-                        + suggestion.text.max_point()
-                        + (fold_point - suggestion_point_start),
-                )
-            } else {
-                let point_in_suggestion = suggestion
-                    .text
-                    .offset_to_point(offset.0 - suggestion.position.0);
-                SuggestionPoint(suggestion_point_start + point_in_suggestion)
-            }
-        } else {
-            SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
-        }
-    }
-
-    pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-
-            if point.0 <= suggestion_start {
-                FoldPoint(point.0)
-            } else if point.0 > suggestion_end {
-                FoldPoint(suggestion_start + (point.0 - suggestion_end))
-            } else {
-                FoldPoint(suggestion_start)
-            }
-        } else {
-            FoldPoint(point.0)
-        }
-    }
-
-    pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-
-            if point.0 <= suggestion_start {
-                SuggestionPoint(point.0)
-            } else {
-                let suggestion_end = suggestion_start + suggestion.text.max_point();
-                SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
-            }
-        } else {
-            SuggestionPoint(point.0)
-        }
-    }
-
-    pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let suggestion_end = suggestion_start + suggestion.text.max_point();
-            let mut summary = TextSummary::default();
-
-            let prefix_range =
-                cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
-            if prefix_range.start < prefix_range.end {
-                summary += self.fold_snapshot.text_summary_for_range(
-                    FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
-                );
-            }
-
-            let suggestion_range =
-                cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
-            if suggestion_range.start < suggestion_range.end {
-                let point_range = suggestion_range.start - suggestion_start
-                    ..suggestion_range.end - suggestion_start;
-                let offset_range = suggestion.text.point_to_offset(point_range.start)
-                    ..suggestion.text.point_to_offset(point_range.end);
-                summary += suggestion
-                    .text
-                    .cursor(offset_range.start)
-                    .summary::<TextSummary>(offset_range.end);
-            }
-
-            let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
-            if suffix_range.start < suffix_range.end {
-                let start = suggestion_start + (suffix_range.start - suggestion_end);
-                let end = suggestion_start + (suffix_range.end - suggestion_end);
-                summary += self
-                    .fold_snapshot
-                    .text_summary_for_range(FoldPoint(start)..FoldPoint(end));
-            }
-
-            summary
-        } else {
-            self.fold_snapshot
-                .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
-        }
-    }
-
-    pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
-        let start = self.to_offset(start);
-        self.chunks(start..self.len(), false, None, None)
-            .flat_map(|chunk| chunk.text.chars())
-    }
-
-    pub fn chunks<'a>(
-        &'a self,
-        range: Range<SuggestionOffset>,
-        language_aware: bool,
-        text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
-    ) -> SuggestionChunks<'a> {
-        if let Some(suggestion) = self.suggestion.as_ref() {
-            let suggestion_range =
-                suggestion.position.0..suggestion.position.0 + suggestion.text.len();
-
-            let prefix_chunks = if range.start.0 < suggestion_range.start {
-                Some(self.fold_snapshot.chunks(
-                    FoldOffset(range.start.0)
-                        ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
-                    language_aware,
-                    text_highlights,
-                ))
-            } else {
-                None
-            };
-
-            let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
-                ..cmp::min(range.end.0, suggestion_range.end);
-            let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
-            {
-                let start = clipped_suggestion_range.start - suggestion_range.start;
-                let end = clipped_suggestion_range.end - suggestion_range.start;
-                Some(suggestion.text.chunks_in_range(start..end))
-            } else {
-                None
-            };
-
-            let suffix_chunks = if range.end.0 > suggestion_range.end {
-                let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
-                let end = range.end.0 - suggestion_range.len();
-                Some(self.fold_snapshot.chunks(
-                    FoldOffset(start)..FoldOffset(end),
-                    language_aware,
-                    text_highlights,
-                ))
-            } else {
-                None
-            };
-
-            SuggestionChunks {
-                prefix_chunks,
-                suggestion_chunks,
-                suffix_chunks,
-                highlight_style: suggestion_highlight,
-            }
-        } else {
-            SuggestionChunks {
-                prefix_chunks: Some(self.fold_snapshot.chunks(
-                    FoldOffset(range.start.0)..FoldOffset(range.end.0),
-                    language_aware,
-                    text_highlights,
-                )),
-                suggestion_chunks: None,
-                suffix_chunks: None,
-                highlight_style: None,
-            }
-        }
-    }
-
-    pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
-        let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
-            let start = suggestion.position.to_point(&self.fold_snapshot).0;
-            let end = start + suggestion.text.max_point();
-            start.row..end.row
-        } else {
-            u32::MAX..u32::MAX
-        };
-
-        let fold_buffer_rows = if row <= suggestion_range.start {
-            self.fold_snapshot.buffer_rows(row)
-        } else if row > suggestion_range.end {
-            self.fold_snapshot
-                .buffer_rows(row - (suggestion_range.end - suggestion_range.start))
-        } else {
-            let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
-            rows.next();
-            rows
-        };
-
-        SuggestionBufferRows {
-            current_row: row,
-            suggestion_row_start: suggestion_range.start,
-            suggestion_row_end: suggestion_range.end,
-            fold_buffer_rows,
-        }
-    }
-
-    #[cfg(test)]
-    pub fn text(&self) -> String {
-        self.chunks(Default::default()..self.len(), false, None, None)
-            .map(|chunk| chunk.text)
-            .collect()
-    }
-}
-
-pub struct SuggestionChunks<'a> {
-    prefix_chunks: Option<FoldChunks<'a>>,
-    suggestion_chunks: Option<text::Chunks<'a>>,
-    suffix_chunks: Option<FoldChunks<'a>>,
-    highlight_style: Option<HighlightStyle>,
-}
-
-impl<'a> Iterator for SuggestionChunks<'a> {
-    type Item = Chunk<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if let Some(chunks) = self.prefix_chunks.as_mut() {
-            if let Some(chunk) = chunks.next() {
-                return Some(chunk);
-            } else {
-                self.prefix_chunks = None;
-            }
-        }
-
-        if let Some(chunks) = self.suggestion_chunks.as_mut() {
-            if let Some(chunk) = chunks.next() {
-                return Some(Chunk {
-                    text: chunk,
-                    highlight_style: self.highlight_style,
-                    ..Default::default()
-                });
-            } else {
-                self.suggestion_chunks = None;
-            }
-        }
-
-        if let Some(chunks) = self.suffix_chunks.as_mut() {
-            if let Some(chunk) = chunks.next() {
-                return Some(chunk);
-            } else {
-                self.suffix_chunks = None;
-            }
-        }
-
-        None
-    }
-}
-
-#[derive(Clone)]
-pub struct SuggestionBufferRows<'a> {
-    current_row: u32,
-    suggestion_row_start: u32,
-    suggestion_row_end: u32,
-    fold_buffer_rows: FoldBufferRows<'a>,
-}
-
-impl<'a> Iterator for SuggestionBufferRows<'a> {
-    type Item = Option<u32>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let row = post_inc(&mut self.current_row);
-        if row <= self.suggestion_row_start || row > self.suggestion_row_end {
-            self.fold_buffer_rows.next()
-        } else {
-            Some(None)
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{display_map::fold_map::FoldMap, MultiBuffer};
-    use gpui::AppContext;
-    use rand::{prelude::StdRng, Rng};
-    use settings::SettingsStore;
-    use std::{
-        env,
-        ops::{Bound, RangeBounds},
-    };
-
-    #[gpui::test]
-    fn test_basic(cx: &mut AppContext) {
-        let buffer = MultiBuffer::build_simple("abcdefghi", cx);
-        let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
-        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-        assert_eq!(suggestion_snapshot.text(), "abcdefghi");
-
-        let (suggestion_snapshot, _, _) = suggestion_map.replace(
-            Some(Suggestion {
-                position: 3,
-                text: "123\n456".into(),
-            }),
-            fold_snapshot,
-            Default::default(),
-        );
-        assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
-
-        buffer.update(cx, |buffer, cx| {
-            buffer.edit(
-                [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
-                None,
-                cx,
-            )
-        });
-        let (fold_snapshot, fold_edits) = fold_map.read(
-            buffer.read(cx).snapshot(cx),
-            buffer_edits.consume().into_inner(),
-        );
-        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
-        assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
-
-        let (mut fold_map_writer, _, _) =
-            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
-        let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
-        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
-        assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
-
-        let (mut fold_map_writer, _, _) =
-            fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
-        let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
-        let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
-        assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
-    }
-
-    #[gpui::test(iterations = 100)]
-    fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
-        init_test(cx);
-
-        let operations = env::var("OPERATIONS")
-            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-            .unwrap_or(10);
-
-        let len = rng.gen_range(0..30);
-        let buffer = if rng.gen() {
-            let text = util::RandomCharIter::new(&mut rng)
-                .take(len)
-                .collect::<String>();
-            MultiBuffer::build_simple(&text, cx)
-        } else {
-            MultiBuffer::build_random(&mut rng, cx)
-        };
-        let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-        log::info!("buffer text: {:?}", buffer_snapshot.text());
-
-        let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-
-        for _ in 0..operations {
-            let mut suggestion_edits = Patch::default();
-
-            let mut prev_suggestion_text = suggestion_snapshot.text();
-            let mut buffer_edits = Vec::new();
-            match rng.gen_range(0..=100) {
-                0..=29 => {
-                    let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
-                    suggestion_edits = suggestion_edits.compose(edits);
-                }
-                30..=59 => {
-                    for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-                        fold_snapshot = new_fold_snapshot;
-                        let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
-                        suggestion_edits = suggestion_edits.compose(edits);
-                    }
-                }
-                _ => buffer.update(cx, |buffer, cx| {
-                    let subscription = buffer.subscribe();
-                    let edit_count = rng.gen_range(1..=5);
-                    buffer.randomly_mutate(&mut rng, edit_count, cx);
-                    buffer_snapshot = buffer.snapshot(cx);
-                    let edits = subscription.consume().into_inner();
-                    log::info!("editing {:?}", edits);
-                    buffer_edits.extend(edits);
-                }),
-            };
-
-            let (new_fold_snapshot, fold_edits) =
-                fold_map.read(buffer_snapshot.clone(), buffer_edits);
-            fold_snapshot = new_fold_snapshot;
-            let (new_suggestion_snapshot, edits) =
-                suggestion_map.sync(fold_snapshot.clone(), fold_edits);
-            suggestion_snapshot = new_suggestion_snapshot;
-            suggestion_edits = suggestion_edits.compose(edits);
-
-            log::info!("buffer text: {:?}", buffer_snapshot.text());
-            log::info!("folds text: {:?}", fold_snapshot.text());
-            log::info!("suggestions text: {:?}", suggestion_snapshot.text());
-
-            let mut expected_text = Rope::from(fold_snapshot.text().as_str());
-            let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
-            if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
-                expected_text.replace(
-                    suggestion.position.0..suggestion.position.0,
-                    &suggestion.text.to_string(),
-                );
-                let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
-                let suggestion_end = suggestion_start + suggestion.text.max_point();
-                expected_buffer_rows.splice(
-                    (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
-                    (0..suggestion_end.row - suggestion_start.row).map(|_| None),
-                );
-            }
-            assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
-            for row_start in 0..expected_buffer_rows.len() {
-                assert_eq!(
-                    suggestion_snapshot
-                        .buffer_rows(row_start as u32)
-                        .collect::<Vec<_>>(),
-                    &expected_buffer_rows[row_start..],
-                    "incorrect buffer rows starting at {}",
-                    row_start
-                );
-            }
-
-            for _ in 0..5 {
-                let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
-                end = expected_text.clip_offset(end, Bias::Right);
-                let mut start = rng.gen_range(0..=end);
-                start = expected_text.clip_offset(start, Bias::Right);
-
-                let actual_text = suggestion_snapshot
-                    .chunks(
-                        SuggestionOffset(start)..SuggestionOffset(end),
-                        false,
-                        None,
-                        None,
-                    )
-                    .map(|chunk| chunk.text)
-                    .collect::<String>();
-                assert_eq!(
-                    actual_text,
-                    expected_text.slice(start..end).to_string(),
-                    "incorrect text in range {:?}",
-                    start..end
-                );
-
-                let start_point = SuggestionPoint(expected_text.offset_to_point(start));
-                let end_point = SuggestionPoint(expected_text.offset_to_point(end));
-                assert_eq!(
-                    suggestion_snapshot.text_summary_for_range(start_point..end_point),
-                    expected_text.slice(start..end).summary()
-                );
-            }
-
-            for edit in suggestion_edits.into_inner() {
-                prev_suggestion_text.replace_range(
-                    edit.new.start.0..edit.new.start.0 + edit.old_len().0,
-                    &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
-                );
-            }
-            assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
-
-            assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
-            assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
-
-            let mut suggestion_point = SuggestionPoint::default();
-            let mut suggestion_offset = SuggestionOffset::default();
-            for ch in expected_text.chars() {
-                assert_eq!(
-                    suggestion_snapshot.to_offset(suggestion_point),
-                    suggestion_offset,
-                    "invalid to_offset({:?})",
-                    suggestion_point
-                );
-                assert_eq!(
-                    suggestion_snapshot.to_point(suggestion_offset),
-                    suggestion_point,
-                    "invalid to_point({:?})",
-                    suggestion_offset
-                );
-                assert_eq!(
-                    suggestion_snapshot
-                        .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
-                    suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
-                );
-
-                let mut bytes = [0; 4];
-                for byte in ch.encode_utf8(&mut bytes).as_bytes() {
-                    suggestion_offset.0 += 1;
-                    if *byte == b'\n' {
-                        suggestion_point.0 += Point::new(1, 0);
-                    } else {
-                        suggestion_point.0 += Point::new(0, 1);
-                    }
-
-                    let clipped_left_point =
-                        suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
-                    let clipped_right_point =
-                        suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
-                    assert!(
-                        clipped_left_point <= clipped_right_point,
-                        "clipped left point {:?} is greater than clipped right point {:?}",
-                        clipped_left_point,
-                        clipped_right_point
-                    );
-                    assert_eq!(
-                        clipped_left_point.0,
-                        expected_text.clip_point(clipped_left_point.0, Bias::Left)
-                    );
-                    assert_eq!(
-                        clipped_right_point.0,
-                        expected_text.clip_point(clipped_right_point.0, Bias::Right)
-                    );
-                    assert!(clipped_left_point <= suggestion_snapshot.max_point());
-                    assert!(clipped_right_point <= suggestion_snapshot.max_point());
-
-                    if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
-                        let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
-                        let suggestion_end = suggestion_start + suggestion.text.max_point();
-                        let invalid_range = (
-                            Bound::Excluded(suggestion_start),
-                            Bound::Included(suggestion_end),
-                        );
-                        assert!(
-                            !invalid_range.contains(&clipped_left_point.0),
-                            "clipped left point {:?} is inside invalid suggestion range {:?}",
-                            clipped_left_point,
-                            invalid_range
-                        );
-                        assert!(
-                            !invalid_range.contains(&clipped_right_point.0),
-                            "clipped right point {:?} is inside invalid suggestion range {:?}",
-                            clipped_right_point,
-                            invalid_range
-                        );
-                    }
-                }
-            }
-        }
-    }
-
-    fn init_test(cx: &mut AppContext) {
-        cx.set_global(SettingsStore::test(cx));
-        theme::init((), cx);
-    }
-
-    impl SuggestionMap {
-        pub fn randomly_mutate(
-            &self,
-            rng: &mut impl Rng,
-        ) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
-            let fold_snapshot = self.0.lock().fold_snapshot.clone();
-            let new_suggestion = if rng.gen_bool(0.3) {
-                None
-            } else {
-                let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
-                let len = rng.gen_range(0..30);
-                Some(Suggestion {
-                    position: index,
-                    text: util::RandomCharIter::new(rng)
-                        .take(len)
-                        .filter(|ch| *ch != '\r')
-                        .collect::<String>()
-                        .as_str()
-                        .into(),
-                })
-            };
-
-            log::info!("replacing suggestion with {:?}", new_suggestion);
-            let (snapshot, edits, _) =
-                self.replace(new_suggestion, fold_snapshot, Default::default());
-            (snapshot, edits)
-        }
-    }
-}

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

@@ -1,80 +1,76 @@
 use super::{
-    suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
+    fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
     TextHighlights,
 };
 use crate::MultiBufferSnapshot;
 use gpui::fonts::HighlightStyle;
 use language::{Chunk, Point};
-use parking_lot::Mutex;
 use std::{cmp, mem, num::NonZeroU32, ops::Range};
 use sum_tree::Bias;
 
 const MAX_EXPANSION_COLUMN: u32 = 256;
 
-pub struct TabMap(Mutex<TabSnapshot>);
+pub struct TabMap(TabSnapshot);
 
 impl TabMap {
-    pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+    pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
         let snapshot = TabSnapshot {
-            suggestion_snapshot: input,
+            fold_snapshot,
             tab_size,
             max_expansion_column: MAX_EXPANSION_COLUMN,
             version: 0,
         };
-        (Self(Mutex::new(snapshot.clone())), snapshot)
+        (Self(snapshot.clone()), snapshot)
     }
 
     #[cfg(test)]
-    pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
-        self.0.lock().max_expansion_column = column;
-        self.0.lock().clone()
+    pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+        self.0.max_expansion_column = column;
+        self.0.clone()
     }
 
     pub fn sync(
-        &self,
-        suggestion_snapshot: SuggestionSnapshot,
-        mut suggestion_edits: Vec<SuggestionEdit>,
+        &mut self,
+        fold_snapshot: FoldSnapshot,
+        mut fold_edits: Vec<FoldEdit>,
         tab_size: NonZeroU32,
     ) -> (TabSnapshot, Vec<TabEdit>) {
-        let mut old_snapshot = self.0.lock();
+        let old_snapshot = &mut self.0;
         let mut new_snapshot = TabSnapshot {
-            suggestion_snapshot,
+            fold_snapshot,
             tab_size,
             max_expansion_column: old_snapshot.max_expansion_column,
             version: old_snapshot.version,
         };
 
-        if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
+        if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
             new_snapshot.version += 1;
         }
 
-        let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
+        let mut tab_edits = Vec::with_capacity(fold_edits.len());
 
         if old_snapshot.tab_size == new_snapshot.tab_size {
             // Expand each edit to include the next tab on the same line as the edit,
             // and any subsequent tabs on that line that moved across the tab expansion
             // boundary.
-            for suggestion_edit in &mut suggestion_edits {
-                let old_end = old_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.old.end);
-                let old_end_row_successor_offset =
-                    old_snapshot.suggestion_snapshot.to_offset(cmp::min(
-                        SuggestionPoint::new(old_end.row() + 1, 0),
-                        old_snapshot.suggestion_snapshot.max_point(),
-                    ));
-                let new_end = new_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.new.end);
+            for fold_edit in &mut fold_edits {
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let old_end_row_successor_offset = cmp::min(
+                    FoldPoint::new(old_end.row() + 1, 0),
+                    old_snapshot.fold_snapshot.max_point(),
+                )
+                .to_offset(&old_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
 
                 let mut offset_from_edit = 0;
                 let mut first_tab_offset = None;
                 let mut last_tab_with_changed_expansion_offset = None;
-                'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
-                    suggestion_edit.old.end..old_end_row_successor_offset,
+                'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+                    fold_edit.old.end..old_end_row_successor_offset,
                     false,
                     None,
                     None,
+                    None,
                 ) {
                     for (ix, _) in chunk.text.match_indices('\t') {
                         let offset_from_edit = offset_from_edit + (ix as u32);
@@ -102,39 +98,31 @@ impl TabMap {
                 }
 
                 if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
-                    suggestion_edit.old.end.0 += offset as usize + 1;
-                    suggestion_edit.new.end.0 += offset as usize + 1;
+                    fold_edit.old.end.0 += offset as usize + 1;
+                    fold_edit.new.end.0 += offset as usize + 1;
                 }
             }
 
             // Combine any edits that overlap due to the expansion.
             let mut ix = 1;
-            while ix < suggestion_edits.len() {
-                let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
+            while ix < fold_edits.len() {
+                let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
                 let prev_edit = prev_edits.last_mut().unwrap();
                 let edit = &next_edits[0];
                 if prev_edit.old.end >= edit.old.start {
                     prev_edit.old.end = edit.old.end;
                     prev_edit.new.end = edit.new.end;
-                    suggestion_edits.remove(ix);
+                    fold_edits.remove(ix);
                 } else {
                     ix += 1;
                 }
             }
 
-            for suggestion_edit in suggestion_edits {
-                let old_start = old_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.old.start);
-                let old_end = old_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.old.end);
-                let new_start = new_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.new.start);
-                let new_end = new_snapshot
-                    .suggestion_snapshot
-                    .to_point(suggestion_edit.new.end);
+            for fold_edit in fold_edits {
+                let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+                let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+                let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+                let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
                 tab_edits.push(TabEdit {
                     old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
                     new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
@@ -155,7 +143,7 @@ impl TabMap {
 
 #[derive(Clone)]
 pub struct TabSnapshot {
-    pub suggestion_snapshot: SuggestionSnapshot,
+    pub fold_snapshot: FoldSnapshot,
     pub tab_size: NonZeroU32,
     pub max_expansion_column: u32,
     pub version: usize,
@@ -163,18 +151,15 @@ pub struct TabSnapshot {
 
 impl TabSnapshot {
     pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
-        self.suggestion_snapshot.buffer_snapshot()
+        &self.fold_snapshot.inlay_snapshot.buffer
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
         let max_point = self.max_point();
         if row < max_point.row() {
-            self.to_tab_point(SuggestionPoint::new(
-                row,
-                self.suggestion_snapshot.line_len(row),
-            ))
-            .0
-            .column
+            self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+                .0
+                .column
         } else {
             max_point.column()
         }
@@ -185,10 +170,10 @@ impl TabSnapshot {
     }
 
     pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
-        let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
-        let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
+        let input_start = self.to_fold_point(range.start, Bias::Left).0;
+        let input_end = self.to_fold_point(range.end, Bias::Right).0;
         let input_summary = self
-            .suggestion_snapshot
+            .fold_snapshot
             .text_summary_for_range(input_start..input_end);
 
         let mut first_line_chars = 0;
@@ -198,7 +183,7 @@ impl TabSnapshot {
             self.max_point()
         };
         for c in self
-            .chunks(range.start..line_end, false, None, None)
+            .chunks(range.start..line_end, false, None, None, None)
             .flat_map(|chunk| chunk.text.chars())
         {
             if c == '\n' {
@@ -217,6 +202,7 @@ impl TabSnapshot {
                     false,
                     None,
                     None,
+                    None,
                 )
                 .flat_map(|chunk| chunk.text.chars())
             {
@@ -238,15 +224,17 @@ impl TabSnapshot {
         range: Range<TabPoint>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> TabChunks<'a> {
         let (input_start, expanded_char_column, to_next_stop) =
-            self.to_suggestion_point(range.start, Bias::Left);
+            self.to_fold_point(range.start, Bias::Left);
         let input_column = input_start.column();
-        let input_start = self.suggestion_snapshot.to_offset(input_start);
+        let input_start = input_start.to_offset(&self.fold_snapshot);
         let input_end = self
-            .suggestion_snapshot
-            .to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
+            .to_fold_point(range.end, Bias::Right)
+            .0
+            .to_offset(&self.fold_snapshot);
         let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
             range.end.column() - range.start.column()
         } else {
@@ -254,11 +242,12 @@ impl TabSnapshot {
         };
 
         TabChunks {
-            suggestion_chunks: self.suggestion_snapshot.chunks(
+            fold_chunks: self.fold_snapshot.chunks(
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                suggestion_highlight,
+                hint_highlights,
+                suggestion_highlights,
             ),
             input_column,
             column: expanded_char_column,
@@ -275,63 +264,58 @@ impl TabSnapshot {
         }
     }
 
-    pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
-        self.suggestion_snapshot.buffer_rows(row)
+    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
+        self.fold_snapshot.buffer_rows(row)
     }
 
     #[cfg(test)]
     pub fn text(&self) -> String {
-        self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
+        self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
             .map(|chunk| chunk.text)
             .collect()
     }
 
     pub fn max_point(&self) -> TabPoint {
-        self.to_tab_point(self.suggestion_snapshot.max_point())
+        self.to_tab_point(self.fold_snapshot.max_point())
     }
 
     pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
         self.to_tab_point(
-            self.suggestion_snapshot
-                .clip_point(self.to_suggestion_point(point, bias).0, bias),
+            self.fold_snapshot
+                .clip_point(self.to_fold_point(point, bias).0, bias),
         )
     }
 
-    pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
-        let chars = self
-            .suggestion_snapshot
-            .chars_at(SuggestionPoint::new(input.row(), 0));
+    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+        let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
         let expanded = self.expand_tabs(chars, input.column());
         TabPoint::new(input.row(), expanded)
     }
 
-    pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
-        let chars = self
-            .suggestion_snapshot
-            .chars_at(SuggestionPoint::new(output.row(), 0));
+    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+        let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
         let expanded = output.column();
         let (collapsed, expanded_char_column, to_next_stop) =
             self.collapse_tabs(chars, expanded, bias);
         (
-            SuggestionPoint::new(output.row(), collapsed as u32),
+            FoldPoint::new(output.row(), collapsed as u32),
             expanded_char_column,
             to_next_stop,
         )
     }
 
     pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
-        let fold_point = self
-            .suggestion_snapshot
-            .fold_snapshot
-            .to_fold_point(point, bias);
-        let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
-        self.to_tab_point(suggestion_point)
+        let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
+        let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+        self.to_tab_point(fold_point)
     }
 
     pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
-        let suggestion_point = self.to_suggestion_point(point, bias).0;
-        let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
-        fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
+        let fold_point = self.to_fold_point(point, bias).0;
+        let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+        self.fold_snapshot
+            .inlay_snapshot
+            .to_buffer_point(inlay_point)
     }
 
     fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
@@ -490,7 +474,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
 const SPACES: &str = "                ";
 
 pub struct TabChunks<'a> {
-    suggestion_chunks: SuggestionChunks<'a>,
+    fold_chunks: FoldChunks<'a>,
     chunk: Chunk<'a>,
     column: u32,
     max_expansion_column: u32,
@@ -506,7 +490,7 @@ impl<'a> Iterator for TabChunks<'a> {
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.chunk.text.is_empty() {
-            if let Some(chunk) = self.suggestion_chunks.next() {
+            if let Some(chunk) = self.fold_chunks.next() {
                 self.chunk = chunk;
                 if self.inside_leading_tab {
                     self.chunk.text = &self.chunk.text[1..];
@@ -574,7 +558,7 @@ impl<'a> Iterator for TabChunks<'a> {
 mod tests {
     use super::*;
     use crate::{
-        display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap},
         MultiBuffer,
     };
     use rand::{prelude::StdRng, Rng};
@@ -583,9 +567,9 @@ mod tests {
     fn test_expand_tabs(cx: &mut gpui::AppContext) {
         let buffer = MultiBuffer::build_simple("", cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
         assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
@@ -600,9 +584,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         tab_snapshot.max_expansion_column = max_expansion_column;
         assert_eq!(tab_snapshot.text(), output);
@@ -615,6 +599,7 @@ mod tests {
                         false,
                         None,
                         None,
+                        None,
                     )
                     .map(|c| c.text)
                     .collect::<String>(),
@@ -626,16 +611,16 @@ mod tests {
                 let input_point = Point::new(0, ix as u32);
                 let output_point = Point::new(0, output.find(c).unwrap() as u32);
                 assert_eq!(
-                    tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
+                    tab_snapshot.to_tab_point(FoldPoint(input_point)),
                     TabPoint(output_point),
                     "to_tab_point({input_point:?})"
                 );
                 assert_eq!(
                     tab_snapshot
-                        .to_suggestion_point(TabPoint(output_point), Bias::Left)
+                        .to_fold_point(TabPoint(output_point), Bias::Left)
                         .0,
-                    SuggestionPoint(input_point),
-                    "to_suggestion_point({output_point:?})"
+                    FoldPoint(input_point),
+                    "to_fold_point({output_point:?})"
                 );
             }
         }
@@ -648,9 +633,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         tab_snapshot.max_expansion_column = max_expansion_column;
         assert_eq!(tab_snapshot.text(), input);
@@ -662,9 +647,9 @@ mod tests {
 
         let buffer = MultiBuffer::build_simple(&input, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
-        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
-        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+        let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+        let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
 
         assert_eq!(
             chunks(&tab_snapshot, TabPoint::zero()),
@@ -689,7 +674,7 @@ mod tests {
             let mut chunks = Vec::new();
             let mut was_tab = false;
             let mut text = String::new();
-            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
+            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
                 if chunk.is_tab != was_tab {
                     if !text.is_empty() {
                         chunks.push((mem::take(&mut text), was_tab));
@@ -721,15 +706,16 @@ mod tests {
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
         log::info!("Buffer text: {:?}", buffer_snapshot.text());
 
-        let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
         fold_map.randomly_mutate(&mut rng);
-        let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
+        let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
         log::info!("FoldMap text: {:?}", fold_snapshot.text());
-        let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
-        let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
-        log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
+        let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
 
-        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
         let tabs_snapshot = tab_map.set_max_expansion_column(32);
 
         let text = text::Rope::from(tabs_snapshot.text().as_str());
@@ -757,7 +743,7 @@ mod tests {
             let expected_summary = TextSummary::from(expected_text.as_str());
             assert_eq!(
                 tabs_snapshot
-                    .chunks(start..end, false, None, None)
+                    .chunks(start..end, false, None, None, None)
                     .map(|c| c.text)
                     .collect::<String>(),
                 expected_text,
@@ -767,7 +753,7 @@ mod tests {
             );
 
             let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
-            if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
+            if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
                 actual_summary.longest_row = expected_summary.longest_row;
                 actual_summary.longest_row_chars = expected_summary.longest_row_chars;
             }

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

@@ -1,5 +1,5 @@
 use super::{
-    suggestion_map::SuggestionBufferRows,
+    fold_map::FoldBufferRows,
     tab_map::{self, TabEdit, TabPoint, TabSnapshot},
     TextHighlights,
 };
@@ -65,7 +65,7 @@ pub struct WrapChunks<'a> {
 
 #[derive(Clone)]
 pub struct WrapBufferRows<'a> {
-    input_buffer_rows: SuggestionBufferRows<'a>,
+    input_buffer_rows: FoldBufferRows<'a>,
     input_buffer_row: Option<u32>,
     output_row: u32,
     soft_wrapped: bool,
@@ -446,6 +446,7 @@ impl WrapSnapshot {
                     false,
                     None,
                     None,
+                    None,
                 );
                 let mut edit_transforms = Vec::<Transform>::new();
                 for _ in edit.new_rows.start..edit.new_rows.end {
@@ -575,7 +576,8 @@ impl WrapSnapshot {
         rows: Range<u32>,
         language_aware: bool,
         text_highlights: Option<&'a TextHighlights>,
-        suggestion_highlight: Option<HighlightStyle>,
+        hint_highlights: Option<HighlightStyle>,
+        suggestion_highlights: Option<HighlightStyle>,
     ) -> WrapChunks<'a> {
         let output_start = WrapPoint::new(rows.start, 0);
         let output_end = WrapPoint::new(rows.end, 0);
@@ -593,7 +595,8 @@ impl WrapSnapshot {
                 input_start..input_end,
                 language_aware,
                 text_highlights,
-                suggestion_highlight,
+                hint_highlights,
+                suggestion_highlights,
             ),
             input_chunk: Default::default(),
             output_position: output_start,
@@ -757,28 +760,18 @@ impl WrapSnapshot {
             }
 
             let text = language::Rope::from(self.text().as_str());
-            let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
+            let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
             let mut expected_buffer_rows = Vec::new();
-            let mut prev_fold_row = 0;
+            let mut prev_tab_row = 0;
             for display_row in 0..=self.max_point().row() {
                 let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
-                let suggestion_point = self
-                    .tab_snapshot
-                    .to_suggestion_point(tab_point, Bias::Left)
-                    .0;
-                let fold_point = self
-                    .tab_snapshot
-                    .suggestion_snapshot
-                    .to_fold_point(suggestion_point);
-                if fold_point.row() == prev_fold_row && display_row != 0 {
+                if tab_point.row() == prev_tab_row && display_row != 0 {
                     expected_buffer_rows.push(None);
                 } else {
-                    let buffer_point = fold_point
-                        .to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
-                    expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
-                    prev_fold_row = fold_point.row();
+                    expected_buffer_rows.push(input_buffer_rows.next().unwrap());
                 }
 
+                prev_tab_row = tab_point.row();
                 assert_eq!(self.line_len(display_row), text.line_len(display_row));
             }
 
@@ -1038,7 +1031,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
 mod tests {
     use super::*;
     use crate::{
-        display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
+        display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
         MultiBuffer,
     };
     use gpui::test::observe;
@@ -1089,11 +1082,11 @@ mod tests {
         });
         let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
         log::info!("Buffer text: {:?}", buffer_snapshot.text());
-        let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+        log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+        let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
         log::info!("FoldMap text: {:?}", fold_snapshot.text());
-        let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
-        log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
-        let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
+        let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
         let tabs_snapshot = tab_map.set_max_expansion_column(32);
         log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
@@ -1122,6 +1115,7 @@ mod tests {
         );
         log::info!("Wrapped text: {:?}", actual_text);
 
+        let mut next_inlay_id = 0;
         let mut edits = Vec::new();
         for _i in 0..operations {
             log::info!("{} ==============================================", _i);
@@ -1139,10 +1133,8 @@ mod tests {
                 }
                 20..=39 => {
                     for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-                        let (suggestion_snapshot, suggestion_edits) =
-                            suggestion_map.sync(fold_snapshot, fold_edits);
                         let (tabs_snapshot, tab_edits) =
-                            tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                            tab_map.sync(fold_snapshot, fold_edits, tab_size);
                         let (mut snapshot, wrap_edits) =
                             wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                         snapshot.check_invariants();
@@ -1151,10 +1143,11 @@ mod tests {
                     }
                 }
                 40..=59 => {
-                    let (suggestion_snapshot, suggestion_edits) =
-                        suggestion_map.randomly_mutate(&mut rng);
+                    let (inlay_snapshot, inlay_edits) =
+                        inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+                    let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
                     let (tabs_snapshot, tab_edits) =
-                        tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+                        tab_map.sync(fold_snapshot, fold_edits, tab_size);
                     let (mut snapshot, wrap_edits) =
                         wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
                     snapshot.check_invariants();
@@ -1173,13 +1166,12 @@ mod tests {
             }
 
             log::info!("Buffer text: {:?}", buffer_snapshot.text());
-            let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
+            let (inlay_snapshot, inlay_edits) =
+                inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+            log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+            let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
             log::info!("FoldMap text: {:?}", fold_snapshot.text());
-            let (suggestion_snapshot, suggestion_edits) =
-                suggestion_map.sync(fold_snapshot, fold_edits);
-            log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
-            let (tabs_snapshot, tab_edits) =
-                tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
+            let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
             log::info!("TabMap text: {:?}", tabs_snapshot.text());
 
             let unwrapped_text = tabs_snapshot.text();
@@ -1227,7 +1219,7 @@ mod tests {
                 if tab_size.get() == 1
                     || !wrapped_snapshot
                         .tab_snapshot
-                        .suggestion_snapshot
+                        .fold_snapshot
                         .text()
                         .contains('\t')
                 {
@@ -1328,8 +1320,14 @@ mod tests {
         }
 
         pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-            self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
-                .map(|h| h.text)
+            self.chunks(
+                wrap_row..self.max_point().row() + 1,
+                false,
+                None,
+                None,
+                None,
+            )
+            .map(|h| h.text)
         }
 
         fn verify_chunks(&mut self, rng: &mut impl Rng) {
@@ -1352,7 +1350,7 @@ mod tests {
                 }
 
                 let actual_text = self
-                    .chunks(start_row..end_row, true, None, None)
+                    .chunks(start_row..end_row, true, None, None, None)
                     .map(|c| c.text)
                     .collect::<String>();
                 assert_eq!(

crates/editor/src/editor.rs 🔗

@@ -2,6 +2,7 @@ mod blink_manager;
 pub mod display_map;
 mod editor_settings;
 mod element;
+mod inlay_hint_cache;
 
 mod git;
 mod highlight_matching_bracket;
@@ -52,11 +53,12 @@ use gpui::{
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
+use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
 pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
-    language_settings::{self, all_language_settings},
+    language_settings::{self, all_language_settings, InlayHintSettings},
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
     Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
     OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@@ -64,11 +66,12 @@ use language::{
 use link_go_to_definition::{
     hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
 };
+use log::error;
+use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
     ToPoint,
 };
-use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
 use scroll::{
@@ -85,12 +88,13 @@ use std::{
     cmp::{self, Ordering, Reverse},
     mem,
     num::NonZeroU32,
-    ops::{Deref, DerefMut, Range},
+    ops::{ControlFlow, Deref, DerefMut, Range},
     path::Path,
     sync::Arc,
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
+use text::Rope;
 use theme::{DiagnosticStyle, Theme, ThemeSettings};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{ItemNavHistory, ViewId, Workspace};
@@ -180,6 +184,12 @@ pub struct GutterHover {
     pub hovered: bool,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+    Suggestion(usize),
+    Hint(usize),
+}
+
 actions!(
     editor,
     [
@@ -535,6 +545,8 @@ pub struct Editor {
     gutter_hovered: bool,
     link_go_to_definition_state: LinkGoToDefinitionState,
     copilot_state: CopilotState,
+    inlay_hint_cache: InlayHintCache,
+    next_inlay_id: usize,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -1056,6 +1068,7 @@ pub struct CopilotState {
     cycled: bool,
     completions: Vec<copilot::Completion>,
     active_completion_index: usize,
+    suggestion: Option<Inlay>,
 }
 
 impl Default for CopilotState {
@@ -1067,6 +1080,7 @@ impl Default for CopilotState {
             completions: Default::default(),
             active_completion_index: 0,
             cycled: false,
+            suggestion: None,
         }
     }
 }
@@ -1181,6 +1195,14 @@ enum GotoDefinitionKind {
     Type,
 }
 
+#[derive(Debug, Copy, Clone)]
+enum InlayRefreshReason {
+    SettingsChange(InlayHintSettings),
+    NewLinesShown,
+    ExcerptEdited,
+    RefreshRequested,
+}
+
 impl Editor {
     pub fn single_line(
         field_editor_style: Option<Arc<GetFieldEditorTheme>>,
@@ -1282,15 +1304,28 @@ impl Editor {
         let soft_wrap_mode_override =
             (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
 
-        let mut project_subscription = None;
-        if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
+        let mut project_subscriptions = Vec::new();
+        if mode == EditorMode::Full {
             if let Some(project) = project.as_ref() {
-                project_subscription = Some(cx.observe(project, |_, _, cx| {
-                    cx.emit(Event::TitleChanged);
-                }))
+                if buffer.read(cx).is_singleton() {
+                    project_subscriptions.push(cx.observe(project, |_, _, cx| {
+                        cx.emit(Event::TitleChanged);
+                    }));
+                }
+                project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
+                    if let project::Event::RefreshInlays = event {
+                        editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
+                    };
+                }));
             }
         }
 
+        let inlay_hint_settings = inlay_hint_settings(
+            selections.newest_anchor().head(),
+            &buffer.read(cx).snapshot(cx),
+            cx,
+        );
+
         let mut this = Self {
             handle: cx.weak_handle(),
             buffer: buffer.clone(),
@@ -1324,6 +1359,7 @@ impl Editor {
                 .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
             completion_tasks: Default::default(),
             next_completion_id: 0,
+            next_inlay_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
             document_highlights_task: Default::default(),
@@ -1340,6 +1376,7 @@ impl Editor {
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
             copilot_state: Default::default(),
+            inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
             gutter_hovered: false,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
@@ -1350,9 +1387,7 @@ impl Editor {
             ],
         };
 
-        if let Some(project_subscription) = project_subscription {
-            this._subscriptions.push(project_subscription);
-        }
+        this._subscriptions.extend(project_subscriptions);
 
         this.end_selection(cx);
         this.scroll_manager.show_scrollbar(cx);
@@ -1873,7 +1908,7 @@ impl Editor {
                 s.set_pending(pending, mode);
             });
         } else {
-            log::error!("update_selection dispatched with no pending selection");
+            error!("update_selection dispatched with no pending selection");
             return;
         }
 
@@ -2577,6 +2612,106 @@ impl Editor {
         }
     }
 
+    fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
+        if self.project.is_none() || self.mode != EditorMode::Full {
+            return;
+        }
+
+        let invalidate_cache = match reason {
+            InlayRefreshReason::SettingsChange(new_settings) => {
+                match self.inlay_hint_cache.update_settings(
+                    &self.buffer,
+                    new_settings,
+                    self.visible_inlay_hints(cx),
+                    cx,
+                ) {
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove,
+                        to_insert,
+                    })) => {
+                        self.splice_inlay_hints(to_remove, to_insert, cx);
+                        return;
+                    }
+                    ControlFlow::Break(None) => return,
+                    ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
+                }
+            }
+            InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
+            InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
+            InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
+        };
+
+        self.inlay_hint_cache.refresh_inlay_hints(
+            self.excerpt_visible_offsets(cx),
+            invalidate_cache,
+            cx,
+        )
+    }
+
+    fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .filter(move |inlay| {
+                Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
+            })
+            .cloned()
+            .collect()
+    }
+
+    fn excerpt_visible_offsets(
+        &self,
+        cx: &mut ViewContext<'_, '_, Editor>,
+    ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
+        let multi_buffer = self.buffer().read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+        let multi_buffer_visible_start = self
+            .scroll_manager
+            .anchor()
+            .anchor
+            .to_point(&multi_buffer_snapshot);
+        let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
+            multi_buffer_visible_start
+                + 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
+            .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+            .into_iter()
+            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+            .map(|(buffer, excerpt_visible_range, excerpt_id)| {
+                (excerpt_id, (buffer, excerpt_visible_range))
+            })
+            .collect()
+    }
+
+    fn splice_inlay_hints(
+        &self,
+        to_remove: Vec<InlayId>,
+        to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let buffer = self.buffer.read(cx).read(cx);
+        let new_inlays = to_insert
+            .into_iter()
+            .map(|(position, id, hint)| {
+                let mut text = hint.text();
+                if hint.padding_right {
+                    text.push(' ');
+                }
+                if hint.padding_left {
+                    text.insert(0, ' ');
+                }
+                (id, InlayProperties { position, text })
+            })
+            .collect();
+        drop(buffer);
+        self.display_map.update(cx, |display_map, cx| {
+            display_map.splice_inlays(to_remove, new_inlays, cx);
+        });
+    }
+
     fn trigger_on_type_formatting(
         &self,
         input: String,
@@ -3227,10 +3362,7 @@ impl Editor {
     }
 
     fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(suggestion) = self
-            .display_map
-            .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
-        {
+        if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
             if let Some((copilot, completion)) =
                 Copilot::global(cx).zip(self.copilot_state.active_completion())
             {
@@ -3249,7 +3381,7 @@ impl Editor {
     }
 
     fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if self.has_active_copilot_suggestion(cx) {
+        if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
             if let Some(copilot) = Copilot::global(cx) {
                 copilot
                     .update(cx, |copilot, cx| {
@@ -3260,8 +3392,9 @@ impl Editor {
                 self.report_copilot_event(None, false, cx)
             }
 
-            self.display_map
-                .update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
+            self.display_map.update(cx, |map, cx| {
+                map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
+            });
             cx.notify();
             true
         } else {
@@ -3282,7 +3415,26 @@ impl Editor {
     }
 
     fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
-        self.display_map.read(cx).has_suggestion()
+        if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+            let buffer = self.buffer.read(cx).read(cx);
+            suggestion.position.is_valid(&buffer)
+        } else {
+            false
+        }
+    }
+
+    fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+        let suggestion = self.copilot_state.suggestion.take()?;
+        self.display_map.update(cx, |map, cx| {
+            map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
+        });
+        let buffer = self.buffer.read(cx).read(cx);
+
+        if suggestion.position.is_valid(&buffer) {
+            Some(suggestion)
+        } else {
+            None
+        }
     }
 
     fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
@@ -3299,14 +3451,27 @@ impl Editor {
             .copilot_state
             .text_for_active_completion(cursor, &snapshot)
         {
+            let text = Rope::from(text);
+            let mut to_remove = Vec::new();
+            if let Some(suggestion) = self.copilot_state.suggestion.take() {
+                to_remove.push(suggestion.id);
+            }
+
+            let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
+            let to_insert = vec![(
+                suggestion_inlay_id,
+                InlayProperties {
+                    position: cursor,
+                    text: text.clone(),
+                },
+            )];
             self.display_map.update(cx, move |map, cx| {
-                map.replace_suggestion(
-                    Some(Suggestion {
-                        position: cursor,
-                        text: text.trim_end().into(),
-                    }),
-                    cx,
-                )
+                map.splice_inlays(to_remove, to_insert, cx)
+            });
+            self.copilot_state.suggestion = Some(Inlay {
+                id: suggestion_inlay_id,
+                position: cursor,
+                text,
             });
             cx.notify();
         } else {
@@ -6641,7 +6806,7 @@ impl Editor {
             if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
                 *end_selections = Some(self.selections.disjoint_anchors());
             } else {
-                log::error!("unexpectedly ended a transaction that wasn't started by this editor");
+                error!("unexpectedly ended a transaction that wasn't started by this editor");
             }
 
             cx.emit(Event::Edited);
@@ -7103,6 +7268,7 @@ impl Editor {
                     self.update_visible_copilot_suggestion(cx);
                 }
                 cx.emit(Event::BufferEdited);
+                self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
             }
             multi_buffer::Event::ExcerptsAdded {
                 buffer,
@@ -7127,7 +7293,7 @@ impl Editor {
                 self.refresh_active_diagnostics(cx);
             }
             _ => {}
-        }
+        };
     }
 
     fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
@@ -7136,6 +7302,14 @@ impl Editor {
 
     fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
         self.refresh_copilot_suggestions(true, cx);
+        self.refresh_inlays(
+            InlayRefreshReason::SettingsChange(inlay_hint_settings(
+                self.selections.newest_anchor().head(),
+                &self.buffer.read(cx).snapshot(cx),
+                cx,
+            )),
+            cx,
+        );
     }
 
     pub fn set_searchable(&mut self, searchable: bool) {
@@ -7425,6 +7599,23 @@ impl Editor {
         let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; };
         cx.write_to_clipboard(ClipboardItem::new(lines));
     }
+
+    pub fn inlay_hint_cache(&self) -> &InlayHintCache {
+        &self.inlay_hint_cache
+    }
+}
+
+fn inlay_hint_settings(
+    location: Anchor,
+    snapshot: &MultiBufferSnapshot,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) -> InlayHintSettings {
+    let file = snapshot.file_at(location);
+    let language = snapshot.language_at(location);
+    let settings = all_language_settings(file, cx);
+    settings
+        .language(language.map(|l| l.name()).as_deref())
+        .inlay_hints
 }
 
 fn consume_contiguous_rows(

crates/editor/src/element.rs 🔗

@@ -1392,7 +1392,12 @@ impl EditorElement {
         } else {
             let style = &self.style;
             let chunks = snapshot
-                .chunks(rows.clone(), true, Some(style.theme.suggestion))
+                .chunks(
+                    rows.clone(),
+                    true,
+                    Some(style.theme.hint),
+                    Some(style.theme.suggestion),
+                )
                 .map(|chunk| {
                     let mut highlight_style = chunk
                         .syntax_highlight_id
@@ -1921,7 +1926,7 @@ impl Element<Editor> for EditorElement {
         let em_advance = style.text.em_advance(cx.font_cache());
         let overscroll = vec2f(em_width, 0.);
         let snapshot = {
-            editor.set_visible_line_count(size.y() / line_height);
+            editor.set_visible_line_count(size.y() / line_height, cx);
 
             let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
             let wrap_width = match editor.soft_wrap_mode(cx) {

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -0,0 +1,2021 @@
+use std::{
+    cmp,
+    ops::{ControlFlow, Range},
+    sync::Arc,
+};
+
+use crate::{
+    display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
+};
+use anyhow::Context;
+use clock::Global;
+use gpui::{ModelHandle, Task, ViewContext};
+use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
+use log::error;
+use parking_lot::RwLock;
+use project::InlayHint;
+
+use collections::{hash_map, HashMap, HashSet};
+use language::language_settings::InlayHintSettings;
+use util::post_inc;
+
+pub struct InlayHintCache {
+    pub hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+    pub allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+    pub version: usize,
+    pub enabled: bool,
+    update_tasks: HashMap<ExcerptId, UpdateTask>,
+}
+
+#[derive(Debug)]
+pub struct CachedExcerptHints {
+    version: usize,
+    buffer_version: Global,
+    buffer_id: u64,
+    pub hints: Vec<(InlayId, InlayHint)>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+    RefreshRequested,
+    ExcerptEdited,
+    None,
+}
+
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+    pub to_remove: Vec<InlayId>,
+    pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
+}
+
+struct UpdateTask {
+    invalidate: InvalidationStrategy,
+    cache_version: usize,
+    task: RunningTask,
+    pending_refresh: Option<ExcerptQuery>,
+}
+
+struct RunningTask {
+    _task: Task<()>,
+    is_running_rx: smol::channel::Receiver<()>,
+}
+
+#[derive(Debug)]
+struct ExcerptHintsUpdate {
+    excerpt_id: ExcerptId,
+    remove_from_visible: Vec<InlayId>,
+    remove_from_cache: HashSet<InlayId>,
+    add_to_cache: HashSet<InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptQuery {
+    buffer_id: u64,
+    excerpt_id: ExcerptId,
+    dimensions: ExcerptDimensions,
+    cache_version: usize,
+    invalidate: InvalidationStrategy,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptDimensions {
+    excerpt_range_start: language::Anchor,
+    excerpt_range_end: language::Anchor,
+    excerpt_visible_range_start: language::Anchor,
+    excerpt_visible_range_end: language::Anchor,
+}
+
+struct HintFetchRanges {
+    visible_range: Range<language::Anchor>,
+    other_ranges: Vec<Range<language::Anchor>>,
+}
+
+impl InvalidationStrategy {
+    fn should_invalidate(&self) -> bool {
+        matches!(
+            self,
+            InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited
+        )
+    }
+}
+
+impl ExcerptQuery {
+    fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges {
+        let visible_range =
+            self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end;
+        let mut other_ranges = Vec::new();
+        if self
+            .dimensions
+            .excerpt_range_start
+            .cmp(&visible_range.start, buffer)
+            .is_lt()
+        {
+            let mut end = visible_range.start;
+            end.offset -= 1;
+            other_ranges.push(self.dimensions.excerpt_range_start..end);
+        }
+        if self
+            .dimensions
+            .excerpt_range_end
+            .cmp(&visible_range.end, buffer)
+            .is_gt()
+        {
+            let mut start = visible_range.end;
+            start.offset += 1;
+            other_ranges.push(start..self.dimensions.excerpt_range_end);
+        }
+
+        HintFetchRanges {
+            visible_range,
+            other_ranges: other_ranges.into_iter().map(|range| range).collect(),
+        }
+    }
+}
+
+impl InlayHintCache {
+    pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
+        Self {
+            allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
+            enabled: inlay_hint_settings.enabled,
+            hints: HashMap::default(),
+            update_tasks: HashMap::default(),
+            version: 0,
+        }
+    }
+
+    pub fn update_settings(
+        &mut self,
+        multi_buffer: &ModelHandle<MultiBuffer>,
+        new_hint_settings: InlayHintSettings,
+        visible_hints: Vec<Inlay>,
+        cx: &mut ViewContext<Editor>,
+    ) -> ControlFlow<Option<InlaySplice>> {
+        let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
+        match (self.enabled, new_hint_settings.enabled) {
+            (false, false) => {
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                ControlFlow::Break(None)
+            }
+            (true, true) => {
+                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.update_tasks.clear();
+                        self.allowed_hint_kinds = new_allowed_hint_kinds;
+                    }
+                    ControlFlow::Break(new_splice)
+                }
+            }
+            (true, false) => {
+                self.enabled = new_hint_settings.enabled;
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                if self.hints.is_empty() {
+                    ControlFlow::Break(None)
+                } else {
+                    self.clear();
+                    ControlFlow::Break(Some(InlaySplice {
+                        to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
+                        to_insert: Vec::new(),
+                    }))
+                }
+            }
+            (false, true) => {
+                self.enabled = new_hint_settings.enabled;
+                self.allowed_hint_kinds = new_allowed_hint_kinds;
+                ControlFlow::Continue(())
+            }
+        }
+    }
+
+    pub fn refresh_inlay_hints(
+        &mut self,
+        mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+        invalidate: InvalidationStrategy,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        if !self.enabled || excerpts_to_query.is_empty() {
+            return;
+        }
+        let update_tasks = &mut self.update_tasks;
+        if invalidate.should_invalidate() {
+            update_tasks
+                .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+        }
+        let cache_version = self.version;
+        excerpts_to_query.retain(|visible_excerpt_id, _| {
+            match update_tasks.entry(*visible_excerpt_id) {
+                hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) {
+                    cmp::Ordering::Less => true,
+                    cmp::Ordering::Equal => invalidate.should_invalidate(),
+                    cmp::Ordering::Greater => false,
+                },
+                hash_map::Entry::Vacant(_) => true,
+            }
+        });
+
+        cx.spawn(|editor, mut cx| async move {
+            editor
+                .update(&mut cx, |editor, cx| {
+                    spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx)
+                })
+                .ok();
+        })
+        .detach();
+    }
+
+    fn new_allowed_hint_kinds_splice(
+        &self,
+        multi_buffer: &ModelHandle<MultiBuffer>,
+        visible_hints: &[Inlay],
+        new_kinds: &HashSet<Option<InlayHintKind>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Option<InlaySplice> {
+        let old_kinds = &self.allowed_hint_kinds;
+        if new_kinds == old_kinds {
+            return None;
+        }
+
+        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 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.hints.iter().fuse().peekable();
+            shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
+                let Some(buffer) = shown_anchor
+                    .buffer_id
+                    .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false };
+                let buffer_snapshot = buffer.read(cx).snapshot();
+                loop {
+                    match excerpt_cache.peek() {
+                        Some((cached_hint_id, cached_hint)) => {
+                            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)
+                                    {
+                                        to_insert.push((
+                                            multi_buffer_snapshot.anchor_in_excerpt(
+                                                *excerpt_id,
+                                                cached_hint.position,
+                                            ),
+                                            *cached_hint_id,
+                                            cached_hint.clone(),
+                                        ));
+                                    }
+                                    excerpt_cache.next();
+                                }
+                                cmp::Ordering::Greater => return true,
+                            }
+                        }
+                        None => return true,
+                    }
+                }
+            });
+
+            for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
+                let cached_hint_kind = maybe_missed_cached_hint.kind;
+                if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
+                    to_insert.push((
+                        multi_buffer_snapshot
+                            .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
+                        *cached_hint_id,
+                        maybe_missed_cached_hint.clone(),
+                    ));
+                }
+            }
+        }
+
+        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,
+            })
+        }
+    }
+
+    fn clear(&mut self) {
+        self.version += 1;
+        self.update_tasks.clear();
+        self.hints.clear();
+    }
+}
+
+fn spawn_new_update_tasks(
+    editor: &mut Editor,
+    excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+    invalidate: InvalidationStrategy,
+    update_cache_version: usize,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) {
+    let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+    for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
+        if !excerpt_visible_range.is_empty() {
+            let buffer = buffer_handle.read(cx);
+            let buffer_snapshot = buffer.snapshot();
+            let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+            if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+                let new_task_buffer_version = buffer_snapshot.version();
+                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)
+                {
+                    return;
+                }
+                if !new_task_buffer_version.changed_since(&cached_buffer_version)
+                    && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
+                {
+                    return;
+                }
+            };
+
+            let buffer_id = buffer.remote_id();
+            let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
+            let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
+
+            let (multi_buffer_snapshot, full_excerpt_range) =
+                editor.buffer.update(cx, |multi_buffer, cx| {
+                    let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+                    (
+                        multi_buffer_snapshot,
+                        multi_buffer
+                            .excerpts_for_buffer(&buffer_handle, cx)
+                            .into_iter()
+                            .find(|(id, _)| id == &excerpt_id)
+                            .map(|(_, range)| range.context),
+                    )
+                });
+
+            if let Some(full_excerpt_range) = full_excerpt_range {
+                let query = ExcerptQuery {
+                    buffer_id,
+                    excerpt_id,
+                    dimensions: ExcerptDimensions {
+                        excerpt_range_start: full_excerpt_range.start,
+                        excerpt_range_end: full_excerpt_range.end,
+                        excerpt_visible_range_start,
+                        excerpt_visible_range_end,
+                    },
+                    cache_version: update_cache_version,
+                    invalidate,
+                };
+
+                let new_update_task = |is_refresh_after_regular_task| {
+                    new_update_task(
+                        query,
+                        multi_buffer_snapshot,
+                        buffer_snapshot,
+                        Arc::clone(&visible_hints),
+                        cached_excerpt_hints,
+                        is_refresh_after_regular_task,
+                        cx,
+                    )
+                };
+                match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+                    hash_map::Entry::Occupied(mut o) => {
+                        let update_task = o.get_mut();
+                        match (update_task.invalidate, invalidate) {
+                            (_, InvalidationStrategy::None) => {}
+                            (
+                                InvalidationStrategy::ExcerptEdited,
+                                InvalidationStrategy::RefreshRequested,
+                            ) if !update_task.task.is_running_rx.is_closed() => {
+                                update_task.pending_refresh = Some(query);
+                            }
+                            _ => {
+                                o.insert(UpdateTask {
+                                    invalidate,
+                                    cache_version: query.cache_version,
+                                    task: new_update_task(false),
+                                    pending_refresh: None,
+                                });
+                            }
+                        }
+                    }
+                    hash_map::Entry::Vacant(v) => {
+                        v.insert(UpdateTask {
+                            invalidate,
+                            cache_version: query.cache_version,
+                            task: new_update_task(false),
+                            pending_refresh: None,
+                        });
+                    }
+                }
+            }
+        }
+    }
+}
+
+fn new_update_task(
+    query: ExcerptQuery,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    buffer_snapshot: BufferSnapshot,
+    visible_hints: Arc<Vec<Inlay>>,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    is_refresh_after_regular_task: bool,
+    cx: &mut ViewContext<'_, '_, Editor>,
+) -> RunningTask {
+    let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot);
+    let (is_running_tx, is_running_rx) = smol::channel::bounded(1);
+    let _task = cx.spawn(|editor, mut cx| async move {
+        let _is_running_tx = is_running_tx;
+        let create_update_task = |range| {
+            fetch_and_update_hints(
+                editor.clone(),
+                multi_buffer_snapshot.clone(),
+                buffer_snapshot.clone(),
+                Arc::clone(&visible_hints),
+                cached_excerpt_hints.as_ref().map(Arc::clone),
+                query,
+                range,
+                cx.clone(),
+            )
+        };
+
+        if is_refresh_after_regular_task {
+            let visible_range_has_updates =
+                match create_update_task(hints_fetch_ranges.visible_range).await {
+                    Ok(updated) => updated,
+                    Err(e) => {
+                        error!("inlay hint visible range update task failed: {e:#}");
+                        return;
+                    }
+                };
+
+            if visible_range_has_updates {
+                let other_update_results = futures::future::join_all(
+                    hints_fetch_ranges
+                        .other_ranges
+                        .into_iter()
+                        .map(create_update_task),
+                )
+                .await;
+
+                for result in other_update_results {
+                    if let Err(e) = result {
+                        error!("inlay hint update task failed: {e:#}");
+                    }
+                }
+            }
+        } else {
+            let task_update_results = futures::future::join_all(
+                std::iter::once(hints_fetch_ranges.visible_range)
+                    .chain(hints_fetch_ranges.other_ranges.into_iter())
+                    .map(create_update_task),
+            )
+            .await;
+
+            for result in task_update_results {
+                if let Err(e) = result {
+                    error!("inlay hint update task failed: {e:#}");
+                }
+            }
+        }
+
+        editor
+            .update(&mut cx, |editor, cx| {
+                let pending_refresh_query = editor
+                    .inlay_hint_cache
+                    .update_tasks
+                    .get_mut(&query.excerpt_id)
+                    .and_then(|task| task.pending_refresh.take());
+
+                if let Some(pending_refresh_query) = pending_refresh_query {
+                    let refresh_multi_buffer = editor.buffer().read(cx);
+                    let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx);
+                    let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+                    let refresh_cached_excerpt_hints = editor
+                        .inlay_hint_cache
+                        .hints
+                        .get(&pending_refresh_query.excerpt_id)
+                        .map(Arc::clone);
+                    if let Some(buffer) =
+                        refresh_multi_buffer.buffer(pending_refresh_query.buffer_id)
+                    {
+                        drop(refresh_multi_buffer);
+                        editor.inlay_hint_cache.update_tasks.insert(
+                            pending_refresh_query.excerpt_id,
+                            UpdateTask {
+                                invalidate: InvalidationStrategy::RefreshRequested,
+                                cache_version: editor.inlay_hint_cache.version,
+                                task: new_update_task(
+                                    pending_refresh_query,
+                                    refresh_multi_buffer_snapshot,
+                                    buffer.read(cx).snapshot(),
+                                    refresh_visible_hints,
+                                    refresh_cached_excerpt_hints,
+                                    true,
+                                    cx,
+                                ),
+                                pending_refresh: None,
+                            },
+                        );
+                    }
+                }
+            })
+            .ok();
+    });
+
+    RunningTask {
+        _task,
+        is_running_rx,
+    }
+}
+
+async fn fetch_and_update_hints(
+    editor: gpui::WeakViewHandle<Editor>,
+    multi_buffer_snapshot: MultiBufferSnapshot,
+    buffer_snapshot: BufferSnapshot,
+    visible_hints: Arc<Vec<Inlay>>,
+    cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+    query: ExcerptQuery,
+    fetch_range: Range<language::Anchor>,
+    mut cx: gpui::AsyncAppContext,
+) -> anyhow::Result<bool> {
+    let inlay_hints_fetch_task = editor
+        .update(&mut cx, |editor, cx| {
+            editor
+                .buffer()
+                .read(cx)
+                .buffer(query.buffer_id)
+                .and_then(|buffer| {
+                    let project = editor.project.as_ref()?;
+                    Some(project.update(cx, |project, cx| {
+                        project.inlay_hints(buffer, fetch_range.clone(), cx)
+                    }))
+                })
+        })
+        .ok()
+        .flatten();
+    let mut update_happened = false;
+    let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) };
+    let new_hints = inlay_hints_fetch_task
+        .await
+        .context("inlay hint fetch task")?;
+    let background_task_buffer_snapshot = buffer_snapshot.clone();
+    let backround_fetch_range = fetch_range.clone();
+    let new_update = cx
+        .background()
+        .spawn(async move {
+            calculate_hint_updates(
+                query,
+                backround_fetch_range,
+                new_hints,
+                &background_task_buffer_snapshot,
+                cached_excerpt_hints,
+                &visible_hints,
+            )
+        })
+        .await;
+
+    editor
+        .update(&mut cx, |editor, cx| {
+            if let Some(new_update) = new_update {
+                update_happened = !new_update.add_to_cache.is_empty()
+                    || !new_update.remove_from_cache.is_empty()
+                    || !new_update.remove_from_visible.is_empty();
+
+                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,
+                            hints: Vec::new(),
+                        }))
+                    });
+                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;
+                    }
+                }
+                cached_excerpt_hints
+                    .hints
+                    .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id));
+                cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+                editor.inlay_hint_cache.version += 1;
+
+                let mut splice = InlaySplice {
+                    to_remove: new_update.remove_from_visible,
+                    to_insert: Vec::new(),
+                };
+
+                for new_hint in new_update.add_to_cache {
+                    let new_hint_position = multi_buffer_snapshot
+                        .anchor_in_excerpt(query.excerpt_id, new_hint.position);
+                    let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
+                    if editor
+                        .inlay_hint_cache
+                        .allowed_hint_kinds
+                        .contains(&new_hint.kind)
+                    {
+                        splice
+                            .to_insert
+                            .push((new_hint_position, new_inlay_id, new_hint.clone()));
+                    }
+
+                    cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
+                }
+
+                cached_excerpt_hints
+                    .hints
+                    .sort_by(|(_, hint_a), (_, hint_b)| {
+                        hint_a.position.cmp(&hint_b.position, &buffer_snapshot)
+                    });
+                drop(cached_excerpt_hints);
+
+                if query.invalidate.should_invalidate() {
+                    let mut outdated_excerpt_caches = HashSet::default();
+                    for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
+                        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.hints.iter().map(|(id, _)| id));
+                        }
+                    }
+                    editor
+                        .inlay_hint_cache
+                        .hints
+                        .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+                }
+
+                let InlaySplice {
+                    to_remove,
+                    to_insert,
+                } = splice;
+                if !to_remove.is_empty() || !to_insert.is_empty() {
+                    editor.splice_inlay_hints(to_remove, to_insert, cx)
+                }
+            }
+        })
+        .ok();
+
+    Ok(update_happened)
+}
+
+fn calculate_hint_updates(
+    query: ExcerptQuery,
+    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: HashSet<InlayHint> = HashSet::default();
+    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.hints.binary_search_by(|probe| {
+                    probe.1.position.cmp(&new_hint.position, buffer_snapshot)
+                }) {
+                    Ok(ix) => {
+                        let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix];
+                        if cached_hint == &new_hint {
+                            excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind);
+                            false
+                        } else {
+                            true
+                        }
+                    }
+                    Err(_) => true,
+                }
+            }
+            None => true,
+        };
+        if missing_from_cache {
+            add_to_cache.insert(new_hint);
+        }
+    }
+
+    let mut remove_from_visible = Vec::new();
+    let mut remove_from_cache = HashSet::default();
+    if query.invalidate.should_invalidate() {
+        remove_from_visible.extend(
+            visible_hints
+                .iter()
+                .filter(|hint| hint.position.excerpt_id == query.excerpt_id)
+                .filter(|hint| {
+                    contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot)
+                })
+                .filter(|hint| {
+                    fetch_range
+                        .start
+                        .cmp(&hint.position.text_anchor, buffer_snapshot)
+                        .is_le()
+                        && fetch_range
+                            .end
+                            .cmp(&hint.position.text_anchor, buffer_snapshot)
+                            .is_ge()
+                })
+                .map(|inlay_hint| inlay_hint.id)
+                .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
+        );
+
+        if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+            let cached_excerpt_hints = cached_excerpt_hints.read();
+            remove_from_cache.extend(
+                cached_excerpt_hints
+                    .hints
+                    .iter()
+                    .filter(|(cached_inlay_id, _)| {
+                        !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+                    })
+                    .filter(|(_, cached_hint)| {
+                        fetch_range
+                            .start
+                            .cmp(&cached_hint.position, buffer_snapshot)
+                            .is_le()
+                            && fetch_range
+                                .end
+                                .cmp(&cached_hint.position, buffer_snapshot)
+                                .is_ge()
+                    })
+                    .map(|(cached_inlay_id, _)| *cached_inlay_id),
+            );
+        }
+    }
+
+    if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
+        None
+    } else {
+        Some(ExcerptHintsUpdate {
+            excerpt_id: query.excerpt_id,
+            remove_from_visible,
+            remove_from_cache,
+            add_to_cache,
+        })
+    }
+}
+
+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()
+}
+
+#[cfg(test)]
+mod tests {
+    use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+
+    use crate::{
+        scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+        serde_json::json,
+        ExcerptRange, InlayHintSettings,
+    };
+    use futures::StreamExt;
+    use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+    use language::{
+        language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+    };
+    use lsp::FakeLanguageServer;
+    use parking_lot::Mutex;
+    use project::{FakeFs, Project};
+    use settings::SettingsStore;
+    use text::Point;
+    use workspace::Workspace;
+
+    use crate::editor_tests::update_test_settings;
+
+    use super::*;
+
+    #[gpui::test]
+    async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    let current_call_id =
+                        Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
+                    for _ in 0..2 {
+                        let mut i = current_call_id;
+                        loop {
+                            new_hints.push(lsp::InlayHint {
+                                position: lsp::Position::new(0, i),
+                                label: lsp::InlayHintLabel::String(i.to_string()),
+                                kind: None,
+                                text_edits: None,
+                                tooltip: None,
+                                padding_left: None,
+                                padding_right: None,
+                                data: None,
+                            });
+                            if i == 0 {
+                                break;
+                            }
+                            i -= 1;
+                        }
+                    }
+
+                    Ok(Some(new_hints))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+            editor.handle_input("some change", cx);
+            edits_made += 1;
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string(), "1".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get new hints after an edit"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        edits_made += 1;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should get new hints after hint refresh/ request"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 1),
+                            label: lsp::InlayHintLabel::String("type hint".to_string()),
+                            kind: Some(lsp::InlayHintKind::TYPE),
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 2),
+                            label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+                            kind: Some(lsp::InlayHintKind::PARAMETER),
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                        lsp::InlayHint {
+                            position: lsp::Position::new(0, 3),
+                            label: lsp::InlayHintLabel::String("other hint".to_string()),
+                            kind: None,
+                            text_edits: None,
+                            tooltip: None,
+                            padding_left: None,
+                            padding_right: None,
+                            data: None,
+                        },
+                    ]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        let mut edits_made = 1;
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                1,
+                "Should query new hints once"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Should get its first hints when opening the editor"
+            );
+            assert_eq!(
+                vec!["other hint".to_string(), "type hint".to_string()],
+                visible_hint_labels(editor, cx)
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+                "Cache should use editor settings to get the allowed hint kinds"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor update the cache version after every cache/view change"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should load new hints twice"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Cached hints should not change due to allowed hint kinds settings update"
+            );
+            assert_eq!(
+                vec!["other hint".to_string(), "type hint".to_string()],
+                visible_hint_labels(editor, cx)
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Should not update cache version due to new loaded hints being the same"
+            );
+        });
+
+        for (new_allowed_hint_kinds, expected_visible_hints) in [
+            (HashSet::from_iter([None]), vec!["other hint".to_string()]),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Type)]),
+                vec!["type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Parameter)]),
+                vec!["parameter hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([None, Some(InlayHintKind::Type)]),
+                vec!["other hint".to_string(), "type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
+                vec!["other hint".to_string(), "parameter hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
+                vec!["parameter hint".to_string(), "type hint".to_string()],
+            ),
+            (
+                HashSet::from_iter([
+                    None,
+                    Some(InlayHintKind::Type),
+                    Some(InlayHintKind::Parameter),
+                ]),
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+            ),
+        ] {
+            edits_made += 1;
+            update_test_settings(cx, |settings| {
+                settings.defaults.inlay_hints = Some(InlayHintSettings {
+                    enabled: true,
+                    show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                    show_parameter_hints: new_allowed_hint_kinds
+                        .contains(&Some(InlayHintKind::Parameter)),
+                    show_other_hints: new_allowed_hint_kinds.contains(&None),
+                })
+            });
+            cx.foreground().run_until_parked();
+            editor.update(cx, |editor, cx| {
+                assert_eq!(
+                    lsp_request_count.load(Ordering::Relaxed),
+                    2,
+                    "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    vec![
+                        "other hint".to_string(),
+                        "parameter hint".to_string(),
+                        "type hint".to_string(),
+                    ],
+                    cached_hint_labels(editor),
+                    "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    expected_visible_hints,
+                    visible_hint_labels(editor, cx),
+                    "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                let inlay_cache = editor.inlay_hint_cache();
+                assert_eq!(
+                    inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
+                    "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
+                );
+                assert_eq!(
+                    inlay_cache.version, edits_made,
+                    "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
+                );
+            });
+        }
+
+        edits_made += 1;
+        let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
+        update_test_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: false,
+                show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: another_allowed_hint_kinds
+                    .contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: another_allowed_hint_kinds.contains(&None),
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should not load new hints when hints got disabled"
+            );
+            assert!(
+                cached_hint_labels(editor).is_empty(),
+                "Should clear the cache when hints got disabled"
+            );
+            assert!(
+                visible_hint_labels(editor, cx).is_empty(),
+                "Should clear visible hints when hints got disabled"
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
+                "Should update its allowed hint kinds even when hints got disabled"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor should update the cache version after hints got disabled"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should not load new hints when they got disabled"
+            );
+            assert!(cached_hint_labels(editor).is_empty());
+            assert!(visible_hint_labels(editor, cx).is_empty());
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "The editor should not update the cache version after /refresh query without updates"
+            );
+        });
+
+        let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
+        edits_made += 1;
+        update_test_settings(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: final_allowed_hint_kinds
+                    .contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: final_allowed_hint_kinds.contains(&None),
+            })
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                3,
+                "Should query for new hints when they got reenabled"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+                "Should get its cached hints fully repopulated after the hints got reenabled"
+            );
+            assert_eq!(
+                vec!["parameter hint".to_string()],
+                visible_hint_labels(editor, cx),
+                "Should get its visible hints repopulated and filtered after the h"
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(
+                inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
+                "Cache should update editor settings when hints got reenabled"
+            );
+            assert_eq!(
+                inlay_cache.version, edits_made,
+                "Cache should update its version after hints got reenabled"
+            );
+        });
+
+        fake_server
+            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .await
+            .expect("inlay refresh request failed");
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                4,
+                "Should query for new hints again"
+            );
+            assert_eq!(
+                vec![
+                    "other hint".to_string(),
+                    "parameter hint".to_string(),
+                    "type hint".to_string(),
+                ],
+                cached_hint_labels(editor),
+            );
+            assert_eq!(
+                vec!["parameter hint".to_string()],
+                visible_hint_labels(editor, cx),
+            );
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, edits_made);
+        });
+    }
+
+    #[gpui::test]
+    async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+        let fake_server = Arc::new(fake_server);
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let another_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+                async move {
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path(file_with_hints).unwrap(),
+                    );
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: lsp::Position::new(0, i),
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+
+        let mut expected_changes = Vec::new();
+        for change_after_opening in [
+            "initial change #1",
+            "initial change #2",
+            "initial change #3",
+        ] {
+            editor.update(cx, |editor, cx| {
+                editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                editor.handle_input(change_after_opening, cx);
+            });
+            expected_changes.push(change_after_opening);
+        }
+
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::Relaxed),
+                2,
+                "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+            );
+            let expected_hints = vec!["2".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 1,
+                "Only one update should be registered in the cache after all cancellations"
+            );
+        });
+
+        let mut edits = Vec::new();
+        for async_later_change in [
+            "another change #1",
+            "another change #2",
+            "another change #3",
+        ] {
+            expected_changes.push(async_later_change);
+            let task_editor = editor.clone();
+            let mut task_cx = cx.clone();
+            edits.push(cx.foreground().spawn(async move {
+                task_editor.update(&mut task_cx, |editor, cx| {
+                    editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+                    editor.handle_input(async_later_change, cx);
+                });
+            }));
+        }
+        let _ = futures::future::join_all(edits).await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let current_text = editor.text(cx);
+            for change in &expected_changes {
+                assert!(
+                    current_text.contains(change),
+                    "Should apply all changes made"
+                );
+            }
+            assert_eq!(
+                lsp_request_count.load(Ordering::SeqCst),
+                3,
+                "Should query new hints one more time, for the last edit only"
+            );
+            let expected_hints = vec!["3".to_string()];
+            assert_eq!(
+                expected_hints,
+                cached_hint_labels(editor),
+                "Should get hints from the last edit landed only"
+            );
+            assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Should update the cache version once more, for the new change"
+            );
+        });
+    }
+
+    #[gpui::test]
+    async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+        let lsp_request_count = Arc::new(AtomicU32::new(0));
+        let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+        let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+                let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Url::from_file_path("/a/main.rs").unwrap(),
+                    );
+
+                    task_lsp_request_ranges.lock().push(params.range);
+                    let query_start = params.range.start;
+                    let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+                    Ok(Some(vec![lsp::InlayHint {
+                        position: query_start,
+                        label: lsp::InlayHintLabel::String(i.to_string()),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    }]))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|range| range.start);
+            assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+            assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line");
+            assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent");
+
+            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2,
+                "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints");
+            let expected_layers = vec!["1".to_string(), "2".to_string()];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "Should have hints from both LSP requests made for a big file"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(
+                inlay_cache.version, 2,
+                "Both LSP queries should've bumped the cache version"
+            );
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+            editor.change_selections(None, cx, |s| s.select_ranges([600..600]));
+            editor.handle_input("++++more text++++", cx);
+        });
+
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+            ranges.sort_by_key(|range| range.start);
+            assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints");
+            assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document");
+            assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end");
+            assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning");
+            assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning");
+            assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line");
+            assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent");
+
+            assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5,
+                "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints");
+            let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "Should have hints from the new LSP response after edit");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
+        });
+    }
+
+    #[gpui::test]
+    async fn test_multiple_excerpts_large_multibuffer(
+        deterministic: Arc<Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+        init_test(cx, |settings| {
+            settings.defaults.inlay_hints = Some(InlayHintSettings {
+                enabled: true,
+                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                show_other_hints: allowed_hint_kinds.contains(&None),
+            })
+        });
+
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+        let language = Arc::new(language);
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+                "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+            }),
+        )
+        .await;
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| {
+            project.languages().add(Arc::clone(&language))
+        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let buffer_1 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "main.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let buffer_2 = project
+            .update(cx, |project, cx| {
+                project.open_buffer((worktree_id, "other.rs"), cx)
+            })
+            .await
+            .unwrap();
+        let multibuffer = cx.add_model(|cx| {
+            let mut multibuffer = MultiBuffer::new(0);
+            multibuffer.push_excerpts(
+                buffer_1.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 0)..Point::new(2, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 0)..Point::new(11, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 0)..Point::new(33, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 0)..Point::new(55, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 0)..Point::new(66, 0),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 0)..Point::new(77, 0),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer.push_excerpts(
+                buffer_2.clone(),
+                [
+                    ExcerptRange {
+                        context: Point::new(0, 1)..Point::new(2, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(4, 1)..Point::new(11, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(22, 1)..Point::new(33, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(44, 1)..Point::new(55, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(56, 1)..Point::new(66, 1),
+                        primary: None,
+                    },
+                    ExcerptRange {
+                        context: Point::new(67, 1)..Point::new(77, 1),
+                        primary: None,
+                    },
+                ],
+                cx,
+            );
+            multibuffer
+        });
+
+        deterministic.run_until_parked();
+        cx.foreground().run_until_parked();
+        let (_, editor) =
+            cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
+        let editor_edited = Arc::new(AtomicBool::new(false));
+        let fake_server = fake_servers.next().await.unwrap();
+        let closure_editor_edited = Arc::clone(&editor_edited);
+        fake_server
+            .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+                let task_editor_edited = Arc::clone(&closure_editor_edited);
+                async move {
+                    let hint_text = if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/main.rs").unwrap()
+                    {
+                        "main hint"
+                    } else if params.text_document.uri
+                        == lsp::Url::from_file_path("/a/other.rs").unwrap()
+                    {
+                        "other hint"
+                    } else {
+                        panic!("unexpected uri: {:?}", params.text_document.uri);
+                    };
+
+                    let positions = [
+                        lsp::Position::new(0, 2),
+                        lsp::Position::new(4, 2),
+                        lsp::Position::new(22, 2),
+                        lsp::Position::new(44, 2),
+                        lsp::Position::new(56, 2),
+                        lsp::Position::new(67, 2),
+                    ];
+                    let out_of_range_hint = lsp::InlayHint {
+                        position: lsp::Position::new(
+                            params.range.start.line + 99,
+                            params.range.start.character + 99,
+                        ),
+                        label: lsp::InlayHintLabel::String(
+                            "out of excerpt range, should be ignored".to_string(),
+                        ),
+                        kind: None,
+                        text_edits: None,
+                        tooltip: None,
+                        padding_left: None,
+                        padding_right: None,
+                        data: None,
+                    };
+
+                    let edited = task_editor_edited.load(Ordering::Acquire);
+                    Ok(Some(
+                        std::iter::once(out_of_range_hint)
+                            .chain(positions.into_iter().enumerate().map(|(i, position)| {
+                                lsp::InlayHint {
+                                    position,
+                                    label: lsp::InlayHintLabel::String(format!(
+                                        "{hint_text}{} #{i}",
+                                        if edited { "(edited)" } else { "" },
+                                    )),
+                                    kind: None,
+                                    text_edits: None,
+                                    tooltip: None,
+                                    padding_left: None,
+                                    padding_right: None,
+                                    data: None,
+                                }
+                            }))
+                            .collect(),
+                    ))
+                }
+            })
+            .next()
+            .await;
+        cx.foreground().run_until_parked();
+
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+            ];
+            assert_eq!(
+                expected_layers,
+                cached_hint_labels(editor),
+                "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+            );
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+            });
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
+            });
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 9);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 12);
+        });
+
+        editor.update(cx, |editor, cx| {
+            editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+                s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+            });
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint #0".to_string(),
+                "main hint #1".to_string(),
+                "main hint #2".to_string(),
+                "main hint #3".to_string(),
+                "main hint #4".to_string(),
+                "main hint #5".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
+        });
+
+        editor_edited.store(true, Ordering::Release);
+        editor.update(cx, |editor, cx| {
+            editor.handle_input("++++more text++++", cx);
+        });
+        cx.foreground().run_until_parked();
+        editor.update(cx, |editor, cx| {
+            let expected_layers = vec![
+                "main hint(edited) #0".to_string(),
+                "main hint(edited) #1".to_string(),
+                "main hint(edited) #2".to_string(),
+                "main hint(edited) #3".to_string(),
+                "other hint #0".to_string(),
+                "other hint #1".to_string(),
+                "other hint #2".to_string(),
+                "other hint #3".to_string(),
+                "other hint #4".to_string(),
+                "other hint #5".to_string(),
+            ];
+            assert_eq!(expected_layers, cached_hint_labels(editor),
+                "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
+unedited (2nd) buffer should have the same hint");
+            assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+            let inlay_cache = editor.inlay_hint_cache();
+            assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
+            assert_eq!(inlay_cache.version, 16);
+        });
+    }
+
+    pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        cx.foreground().forbid_parking();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            client::init_settings(cx);
+            language::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
+            crate::init(cx);
+        });
+
+        update_test_settings(cx, f);
+    }
+
+    async fn prepare_test_objects(
+        cx: &mut TestAppContext,
+    ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_servers = language
+            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+                    ..Default::default()
+                },
+                ..Default::default()
+            }))
+            .await;
+
+        let fs = FakeFs::new(cx.background());
+        fs.insert_tree(
+            "/a",
+            json!({
+                "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+                "other.rs": "// Test file",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, ["/a".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+        let worktree_id = workspace.update(cx, |workspace, cx| {
+            workspace.project().read_with(cx, |project, cx| {
+                project.worktrees(cx).next().unwrap().read(cx).id()
+            })
+        });
+
+        let _buffer = project
+            .update(cx, |project, cx| {
+                project.open_local_buffer("/a/main.rs", cx)
+            })
+            .await
+            .unwrap();
+        cx.foreground().run_until_parked();
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
+        let editor = workspace
+            .update(cx, |workspace, cx| {
+                workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        ("/a/main.rs", editor, fake_server)
+    }
+
+    fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+        let mut labels = Vec::new();
+        for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+            let excerpt_hints = excerpt_hints.read();
+            for (_, inlay) in excerpt_hints.hints.iter() {
+                match &inlay.label {
+                    project::InlayHintLabel::String(s) => labels.push(s.to_string()),
+                    _ => unreachable!(),
+                }
+            }
+        }
+
+        labels.sort();
+        labels
+    }
+
+    fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+        let mut hints = editor
+            .visible_inlay_hints(cx)
+            .into_iter()
+            .map(|hint| hint.text.to_string())
+            .collect::<Vec<_>>();
+        hints.sort();
+        hints
+    }
+}

crates/editor/src/multi_buffer/anchor.rs 🔗

@@ -49,6 +49,10 @@ impl Anchor {
         }
     }
 
+    pub fn bias(&self) -> Bias {
+        self.text_anchor.bias
+    }
+
     pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
         if self.text_anchor.bias != Bias::Left {
             if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
@@ -81,6 +85,19 @@ impl Anchor {
     {
         snapshot.summary_for_anchor(self)
     }
+
+    pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
+        if *self == Anchor::min() || *self == Anchor::max() {
+            true
+        } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
+            excerpt.contains(self)
+                && (self.text_anchor == excerpt.range.context.start
+                    || self.text_anchor == excerpt.range.context.end
+                    || self.text_anchor.is_valid(&excerpt.buffer))
+        } else {
+            false
+        }
+    }
 }
 
 impl ToOffset for Anchor {

crates/editor/src/scroll.rs 🔗

@@ -13,13 +13,14 @@ use gpui::{
 };
 use language::{Bias, Point};
 use util::ResultExt;
-use workspace::WorkspaceId;
+use workspace::{item::Item, WorkspaceId};
 
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
     persistence::DB,
-    Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
+    Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
+    ToPoint,
 };
 
 use self::{
@@ -293,8 +294,19 @@ impl Editor {
         self.scroll_manager.visible_line_count
     }
 
-    pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
-        self.scroll_manager.visible_line_count = Some(lines)
+    pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
+        let opened_first_time = self.scroll_manager.visible_line_count.is_none();
+        self.scroll_manager.visible_line_count = Some(lines);
+        if opened_first_time {
+            cx.spawn(|editor, mut cx| async move {
+                editor
+                    .update(&mut cx, |editor, cx| {
+                        editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
+                    })
+                    .ok()
+            })
+            .detach()
+        }
     }
 
     pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
@@ -320,6 +332,10 @@ impl Editor {
             workspace_id,
             cx,
         );
+
+        if !self.is_singleton(cx) {
+            self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
+        }
     }
 
     pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {

crates/language/src/language_settings.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{File, Language};
 use anyhow::Result;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use globset::GlobMatcher;
 use gpui::AppContext;
 use schemars::{
@@ -52,6 +52,7 @@ pub struct LanguageSettings {
     pub show_copilot_suggestions: bool,
     pub show_whitespaces: ShowWhitespaceSetting,
     pub extend_comment_on_newline: bool,
+    pub inlay_hints: InlayHintSettings,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
     pub show_whitespaces: Option<ShowWhitespaceSetting>,
     #[serde(default)]
     pub extend_comment_on_newline: Option<bool>,
+    #[serde(default)]
+    pub inlay_hints: Option<InlayHintSettings>,
 }
 
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -150,6 +153,38 @@ pub enum Formatter {
     },
 }
 
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct InlayHintSettings {
+    #[serde(default)]
+    pub enabled: bool,
+    #[serde(default = "default_true")]
+    pub show_type_hints: bool,
+    #[serde(default = "default_true")]
+    pub show_parameter_hints: bool,
+    #[serde(default = "default_true")]
+    pub show_other_hints: bool,
+}
+
+fn default_true() -> bool {
+    true
+}
+
+impl InlayHintSettings {
+    pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
+        let mut kinds = HashSet::default();
+        if self.show_type_hints {
+            kinds.insert(Some(InlayHintKind::Type));
+        }
+        if self.show_parameter_hints {
+            kinds.insert(Some(InlayHintKind::Parameter));
+        }
+        if self.show_other_hints {
+            kinds.insert(None);
+        }
+        kinds
+    }
+}
+
 impl AllLanguageSettings {
     pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
         if let Some(name) = language_name {
@@ -184,6 +219,29 @@ impl AllLanguageSettings {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum InlayHintKind {
+    Type,
+    Parameter,
+}
+
+impl InlayHintKind {
+    pub fn from_name(name: &str) -> Option<Self> {
+        match name {
+            "type" => Some(InlayHintKind::Type),
+            "parameter" => Some(InlayHintKind::Parameter),
+            _ => None,
+        }
+    }
+
+    pub fn name(&self) -> &'static str {
+        match self {
+            InlayHintKind::Type => "type",
+            InlayHintKind::Parameter => "parameter",
+        }
+    }
+}
+
 impl settings::Setting for AllLanguageSettings {
     const KEY: Option<&'static str> = None;
 
@@ -347,6 +405,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
         &mut settings.extend_comment_on_newline,
         src.extend_comment_on_newline,
     );
+    merge(&mut settings.inlay_hints, src.inlay_hints);
     fn merge<T>(target: &mut T, value: Option<T>) {
         if let Some(value) = value {
             *target = value;

crates/lsp/src/lsp.rs 🔗

@@ -388,6 +388,9 @@ impl LanguageServer {
                         resolve_support: None,
                         ..WorkspaceSymbolClientCapabilities::default()
                     }),
+                    inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
+                        refresh_support: Some(true),
+                    }),
                     ..Default::default()
                 }),
                 text_document: Some(TextDocumentClientCapabilities {
@@ -429,6 +432,10 @@ impl LanguageServer {
                         content_format: Some(vec![MarkupKind::Markdown]),
                         ..Default::default()
                     }),
+                    inlay_hint: Some(InlayHintClientCapabilities {
+                        resolve_support: None,
+                        dynamic_registration: Some(false),
+                    }),
                     ..Default::default()
                 }),
                 experimental: Some(json!({
@@ -607,6 +614,7 @@ impl LanguageServer {
                                 })
                                 .detach();
                         }
+
                         Err(error) => {
                             log::error!(
                                 "error deserializing {} request: {:?}, message: {:?}",
@@ -708,7 +716,7 @@ impl LanguageServer {
                                         .context("failed to deserialize response"),
                                     Err(error) => Err(anyhow!("{}", error.message)),
                                 };
-                                let _ = tx.send(response);
+                                _ = tx.send(response);
                             })
                             .detach();
                     }),

crates/project/src/lsp_command.rs 🔗

@@ -1,14 +1,15 @@
 use crate::{
-    DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
-    ProjectTransaction,
+    DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
+    InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
+    MarkupContent, Project, ProjectTransaction,
 };
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
 use client::proto::{self, PeerId};
 use fs::LineEnding;
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
-    language_settings::language_settings,
+    language_settings::{language_settings, InlayHintKind},
     point_from_lsp, point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
@@ -126,6 +127,10 @@ pub(crate) struct OnTypeFormatting {
     pub push_to_history: bool,
 }
 
+pub(crate) struct InlayHints {
+    pub range: Range<Anchor>,
+}
+
 pub(crate) struct FormattingOptions {
     tab_size: u32,
 }
@@ -1780,3 +1785,327 @@ impl LspCommand for OnTypeFormatting {
         message.buffer_id
     }
 }
+
+#[async_trait(?Send)]
+impl LspCommand for InlayHints {
+    type Response = Vec<InlayHint>;
+    type LspRequest = lsp::InlayHintRequest;
+    type ProtoRequest = proto::InlayHints;
+
+    fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
+        let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false };
+        match inlay_hint_provider {
+            lsp::OneOf::Left(enabled) => *enabled,
+            lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
+                lsp::InlayHintServerCapabilities::Options(_) => true,
+                lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false,
+            },
+        }
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        buffer: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::InlayHintParams {
+        lsp::InlayHintParams {
+            text_document: lsp::TextDocumentIdentifier {
+                uri: lsp::Url::from_file_path(path).unwrap(),
+            },
+            range: range_to_lsp(self.range.to_point_utf16(buffer)),
+            work_done_progress_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<Vec<lsp::InlayHint>>,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        _: LanguageServerId,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<InlayHint>> {
+        cx.read(|cx| {
+            let origin_buffer = buffer.read(cx);
+            Ok(message
+                .unwrap_or_default()
+                .into_iter()
+                .map(|lsp_hint| {
+                    let kind = lsp_hint.kind.and_then(|kind| match kind {
+                        lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
+                        lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
+                        _ => None,
+                    });
+                    let position = origin_buffer
+                        .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+                    InlayHint {
+                        buffer_id: origin_buffer.remote_id(),
+                        position: if kind == Some(InlayHintKind::Parameter) {
+                            origin_buffer.anchor_before(position)
+                        } else {
+                            origin_buffer.anchor_after(position)
+                        },
+                        padding_left: lsp_hint.padding_left.unwrap_or(false),
+                        padding_right: lsp_hint.padding_right.unwrap_or(false),
+                        label: match lsp_hint.label {
+                            lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
+                            lsp::InlayHintLabel::LabelParts(lsp_parts) => {
+                                InlayHintLabel::LabelParts(
+                                    lsp_parts
+                                        .into_iter()
+                                        .map(|label_part| InlayHintLabelPart {
+                                            value: label_part.value,
+                                            tooltip: label_part.tooltip.map(
+                                                |tooltip| {
+                                                    match tooltip {
+                                        lsp::InlayHintLabelPartTooltip::String(s) => {
+                                            InlayHintLabelPartTooltip::String(s)
+                                        }
+                                        lsp::InlayHintLabelPartTooltip::MarkupContent(
+                                            markup_content,
+                                        ) => InlayHintLabelPartTooltip::MarkupContent(
+                                            MarkupContent {
+                                                kind: format!("{:?}", markup_content.kind),
+                                                value: markup_content.value,
+                                            },
+                                        ),
+                                    }
+                                                },
+                                            ),
+                                            location: label_part.location.map(|lsp_location| {
+                                                let target_start = origin_buffer.clip_point_utf16(
+                                                    point_from_lsp(lsp_location.range.start),
+                                                    Bias::Left,
+                                                );
+                                                let target_end = origin_buffer.clip_point_utf16(
+                                                    point_from_lsp(lsp_location.range.end),
+                                                    Bias::Left,
+                                                );
+                                                Location {
+                                                    buffer: buffer.clone(),
+                                                    range: origin_buffer.anchor_after(target_start)
+                                                        ..origin_buffer.anchor_before(target_end),
+                                                }
+                                            }),
+                                        })
+                                        .collect(),
+                                )
+                            }
+                        },
+                        kind,
+                        tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
+                            lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
+                            lsp::InlayHintTooltip::MarkupContent(markup_content) => {
+                                InlayHintTooltip::MarkupContent(MarkupContent {
+                                    kind: format!("{:?}", markup_content.kind),
+                                    value: markup_content.value,
+                                })
+                            }
+                        }),
+                    }
+                })
+                .collect())
+        })
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
+        proto::InlayHints {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            start: Some(language::proto::serialize_anchor(&self.range.start)),
+            end: Some(language::proto::serialize_anchor(&self.range.end)),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::InlayHints,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let start = message
+            .start
+            .and_then(language::proto::deserialize_anchor)
+            .context("invalid start")?;
+        let end = message
+            .end
+            .and_then(language::proto::deserialize_anchor)
+            .context("invalid end")?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+
+        Ok(Self { range: start..end })
+    }
+
+    fn response_to_proto(
+        response: Vec<InlayHint>,
+        _: &mut Project,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::InlayHintsResponse {
+        proto::InlayHintsResponse {
+            hints: response
+                .into_iter()
+                .map(|response_hint| proto::InlayHint {
+                    position: Some(language::proto::serialize_anchor(&response_hint.position)),
+                    padding_left: response_hint.padding_left,
+                    padding_right: response_hint.padding_right,
+                    label: Some(proto::InlayHintLabel {
+                        label: Some(match response_hint.label {
+                            InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+                            InlayHintLabel::LabelParts(label_parts) => {
+                                proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+                                    parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
+                                        value: label_part.value,
+                                        tooltip: label_part.tooltip.map(|tooltip| {
+                                            let proto_tooltip = match tooltip {
+                                                InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+                                                InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+                                                    kind: markup_content.kind,
+                                                    value: markup_content.value,
+                                                }),
+                                            };
+                                            proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+                                        }),
+                                        location: label_part.location.map(|location| proto::Location {
+                                            start: Some(serialize_anchor(&location.range.start)),
+                                            end: Some(serialize_anchor(&location.range.end)),
+                                            buffer_id: location.buffer.read(cx).remote_id(),
+                                        }),
+                                    }).collect()
+                                })
+                            }
+                        }),
+                    }),
+                    kind: response_hint.kind.map(|kind| kind.name().to_string()),
+                    tooltip: response_hint.tooltip.map(|response_tooltip| {
+                        let proto_tooltip = match response_tooltip {
+                            InlayHintTooltip::String(s) => {
+                                proto::inlay_hint_tooltip::Content::Value(s)
+                            }
+                            InlayHintTooltip::MarkupContent(markup_content) => {
+                                proto::inlay_hint_tooltip::Content::MarkupContent(
+                                    proto::MarkupContent {
+                                        kind: markup_content.kind,
+                                        value: markup_content.value,
+                                    },
+                                )
+                            }
+                        };
+                        proto::InlayHintTooltip {
+                            content: Some(proto_tooltip),
+                        }
+                    }),
+                })
+                .collect(),
+            version: serialize_version(buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::InlayHintsResponse,
+        project: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Vec<InlayHint>> {
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })
+            .await?;
+
+        let mut hints = Vec::new();
+        for message_hint in message.hints {
+            let buffer_id = message_hint
+                .position
+                .as_ref()
+                .and_then(|location| location.buffer_id)
+                .context("missing buffer id")?;
+            let hint = InlayHint {
+                buffer_id,
+                position: message_hint
+                    .position
+                    .and_then(language::proto::deserialize_anchor)
+                    .context("invalid position")?,
+                label: match message_hint
+                    .label
+                    .and_then(|label| label.label)
+                    .context("missing label")?
+                {
+                    proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
+                    proto::inlay_hint_label::Label::LabelParts(parts) => {
+                        let mut label_parts = Vec::new();
+                        for part in parts.parts {
+                            label_parts.push(InlayHintLabelPart {
+                                value: part.value,
+                                tooltip: part.tooltip.map(|tooltip| match tooltip.content {
+                                    Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s),
+                                    Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
+                                        kind: markup_content.kind,
+                                        value: markup_content.value,
+                                    }),
+                                    None => InlayHintLabelPartTooltip::String(String::new()),
+                                }),
+                                location: match part.location {
+                                    Some(location) => {
+                                        let target_buffer = project
+                                            .update(&mut cx, |this, cx| {
+                                                this.wait_for_remote_buffer(location.buffer_id, cx)
+                                            })
+                                            .await?;
+                                        Some(Location {
+                                        range: location
+                                            .start
+                                            .and_then(language::proto::deserialize_anchor)
+                                            .context("invalid start")?
+                                            ..location
+                                                .end
+                                                .and_then(language::proto::deserialize_anchor)
+                                                .context("invalid end")?,
+                                        buffer: target_buffer,
+                                    })},
+                                    None => None,
+                                },
+                            });
+                        }
+
+                        InlayHintLabel::LabelParts(label_parts)
+                    }
+                },
+                padding_left: message_hint.padding_left,
+                padding_right: message_hint.padding_right,
+                kind: message_hint
+                    .kind
+                    .as_deref()
+                    .and_then(InlayHintKind::from_name),
+                tooltip: message_hint.tooltip.and_then(|tooltip| {
+                    Some(match tooltip.content? {
+                        proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
+                        proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
+                            InlayHintTooltip::MarkupContent(MarkupContent {
+                                kind: markup_content.kind,
+                                value: markup_content.value,
+                            })
+                        }
+                    })
+                }),
+            };
+
+            hints.push(hint);
+        }
+
+        Ok(hints)
+    }
+
+    fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 {
+        message.buffer_id
+    }
+}

crates/project/src/project.rs 🔗

@@ -29,14 +29,15 @@ use gpui::{
     AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext,
     ModelHandle, Task, WeakModelHandle,
 };
+use itertools::Itertools;
 use language::{
-    language_settings::{language_settings, FormatOnSave, Formatter},
+    language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version,
     },
-    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
+    range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel,
     Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _,
     Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt,
     Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
@@ -75,6 +76,7 @@ use std::{
     time::{Duration, Instant},
 };
 use terminals::Terminals;
+use text::Anchor;
 use util::{
     debug_panic, defer, http::HttpClient, merge_json_value_into,
     paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
@@ -277,6 +279,7 @@ pub enum Event {
         new_peer_id: proto::PeerId,
     },
     CollaboratorLeft(proto::PeerId),
+    RefreshInlays,
 }
 
 pub enum LanguageServerState {
@@ -319,12 +322,63 @@ pub struct DiagnosticSummary {
     pub warning_count: usize,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Location {
     pub buffer: ModelHandle<Buffer>,
     pub range: Range<language::Anchor>,
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHint {
+    pub buffer_id: u64,
+    pub position: language::Anchor,
+    pub label: InlayHintLabel,
+    pub kind: Option<InlayHintKind>,
+    pub padding_left: bool,
+    pub padding_right: bool,
+    pub tooltip: Option<InlayHintTooltip>,
+}
+
+impl InlayHint {
+    pub fn text(&self) -> String {
+        match &self.label {
+            InlayHintLabel::String(s) => s.to_owned(),
+            InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabel {
+    String(String),
+    LabelParts(Vec<InlayHintLabelPart>),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct InlayHintLabelPart {
+    pub value: String,
+    pub tooltip: Option<InlayHintLabelPartTooltip>,
+    pub location: Option<Location>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintTooltip {
+    String(String),
+    MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum InlayHintLabelPartTooltip {
+    String(String),
+    MarkupContent(MarkupContent),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct MarkupContent {
+    pub kind: String,
+    pub value: String,
+}
+
 #[derive(Debug, Clone)]
 pub struct LocationLink {
     pub origin: Option<Location>,
@@ -485,6 +539,8 @@ impl Project {
         client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
         client.add_model_request_handler(Self::handle_apply_code_action);
         client.add_model_request_handler(Self::handle_on_type_formatting);
+        client.add_model_request_handler(Self::handle_inlay_hints);
+        client.add_model_request_handler(Self::handle_refresh_inlay_hints);
         client.add_model_request_handler(Self::handle_reload_buffers);
         client.add_model_request_handler(Self::handle_synchronize_buffers);
         client.add_model_request_handler(Self::handle_format_buffers);
@@ -2651,10 +2707,11 @@ impl Project {
         cx: &mut AsyncAppContext,
     ) -> Result<Option<Arc<LanguageServer>>> {
         let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
-
         let language_server = match pending_server.task.await? {
             Some(server) => server.initialize(initialization_options).await?,
-            None => return Ok(None),
+            None => {
+                return Ok(None);
+            }
         };
 
         language_server
@@ -2771,6 +2828,24 @@ impl Project {
             })
             .detach();
 
+        language_server
+            .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
+                move |(), mut cx| async move {
+                    let this = this
+                        .upgrade(&cx)
+                        .ok_or_else(|| anyhow!("project dropped"))?;
+                    this.update(&mut cx, |project, cx| {
+                        cx.emit(Event::RefreshInlays);
+                        project.remote_id().map(|project_id| {
+                            project.client.send(proto::RefreshInlayHints { project_id })
+                        })
+                    })
+                    .transpose()?;
+                    Ok(())
+                }
+            })
+            .detach();
+
         let disk_based_diagnostics_progress_token =
             adapter.disk_based_diagnostics_progress_token.clone();
 
@@ -4837,6 +4912,61 @@ impl Project {
         )
     }
 
+    pub fn inlay_hints<T: ToOffset>(
+        &self,
+        buffer_handle: ModelHandle<Buffer>,
+        range: Range<T>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<InlayHint>>> {
+        let buffer = buffer_handle.read(cx);
+        let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
+        let range_start = range.start;
+        let range_end = range.end;
+        let buffer_id = buffer.remote_id();
+        let buffer_version = buffer.version().clone();
+        let lsp_request = InlayHints { range };
+
+        if self.is_local() {
+            let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx);
+            cx.spawn(|_, mut cx| async move {
+                buffer_handle
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp])
+                    })
+                    .await
+                    .context("waiting for inlay hint request range edits")?;
+                lsp_request_task.await.context("inlay hints LSP request")
+            })
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::InlayHints {
+                project_id,
+                buffer_id,
+                start: Some(serialize_anchor(&range_start)),
+                end: Some(serialize_anchor(&range_end)),
+                version: serialize_version(&buffer_version),
+            };
+            cx.spawn(|project, cx| async move {
+                let response = client
+                    .request(request)
+                    .await
+                    .context("inlay hints proto request")?;
+                let hints_request_result = LspCommand::response_from_proto(
+                    lsp_request,
+                    response,
+                    project,
+                    buffer_handle.clone(),
+                    cx,
+                )
+                .await;
+
+                hints_request_result.context("inlay hints proto response conversion")
+            })
+        } else {
+            Task::ready(Err(anyhow!("project does not have a remote id")))
+        }
+    }
+
     #[allow(clippy::type_complexity)]
     pub fn search(
         &self,
@@ -5409,41 +5539,39 @@ impl Project {
 
         let abs_path = worktree_handle.read(cx).abs_path();
         for server_id in &language_server_ids {
-            if let Some(server) = self.language_servers.get(server_id) {
-                if let LanguageServerState::Running {
-                    server,
-                    watched_paths,
-                    ..
-                } = server
-                {
-                    if let Some(watched_paths) = watched_paths.get(&worktree_id) {
-                        let params = lsp::DidChangeWatchedFilesParams {
-                            changes: changes
-                                .iter()
-                                .filter_map(|(path, _, change)| {
-                                    if !watched_paths.is_match(&path) {
-                                        return None;
-                                    }
-                                    let typ = match change {
-                                        PathChange::Loaded => return None,
-                                        PathChange::Added => lsp::FileChangeType::CREATED,
-                                        PathChange::Removed => lsp::FileChangeType::DELETED,
-                                        PathChange::Updated => lsp::FileChangeType::CHANGED,
-                                        PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
-                                    };
-                                    Some(lsp::FileEvent {
-                                        uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(),
-                                        typ,
-                                    })
+            if let Some(LanguageServerState::Running {
+                server,
+                watched_paths,
+                ..
+            }) = self.language_servers.get(server_id)
+            {
+                if let Some(watched_paths) = watched_paths.get(&worktree_id) {
+                    let params = lsp::DidChangeWatchedFilesParams {
+                        changes: changes
+                            .iter()
+                            .filter_map(|(path, _, change)| {
+                                if !watched_paths.is_match(&path) {
+                                    return None;
+                                }
+                                let typ = match change {
+                                    PathChange::Loaded => return None,
+                                    PathChange::Added => lsp::FileChangeType::CREATED,
+                                    PathChange::Removed => lsp::FileChangeType::DELETED,
+                                    PathChange::Updated => lsp::FileChangeType::CHANGED,
+                                    PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED,
+                                };
+                                Some(lsp::FileEvent {
+                                    uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(),
+                                    typ,
                                 })
-                                .collect(),
-                        };
+                            })
+                            .collect(),
+                    };
 
-                        if !params.changes.is_empty() {
-                            server
-                                .notify::<lsp::notification::DidChangeWatchedFiles>(params)
-                                .log_err();
-                        }
+                    if !params.changes.is_empty() {
+                        server
+                            .notify::<lsp::notification::DidChangeWatchedFiles>(params)
+                            .log_err();
                     }
                 }
             }
@@ -6581,6 +6709,68 @@ impl Project {
         Ok(proto::OnTypeFormattingResponse { transaction })
     }
 
+    async fn handle_inlay_hints(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::InlayHints>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::InlayHintsResponse> {
+        let sender_id = envelope.original_sender_id()?;
+        let buffer = this.update(&mut cx, |this, cx| {
+            this.opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade(cx))
+                .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
+        })?;
+        let buffer_version = deserialize_version(&envelope.payload.version);
+
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(buffer_version.clone())
+            })
+            .await
+            .with_context(|| {
+                format!(
+                    "waiting for version {:?} for buffer {}",
+                    buffer_version,
+                    buffer.id()
+                )
+            })?;
+
+        let start = envelope
+            .payload
+            .start
+            .and_then(deserialize_anchor)
+            .context("missing range start")?;
+        let end = envelope
+            .payload
+            .end
+            .and_then(deserialize_anchor)
+            .context("missing range end")?;
+        let buffer_hints = this
+            .update(&mut cx, |project, cx| {
+                project.inlay_hints(buffer, start..end, cx)
+            })
+            .await
+            .context("inlay hints fetch")?;
+
+        Ok(this.update(&mut cx, |project, cx| {
+            InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx)
+        }))
+    }
+
+    async fn handle_refresh_inlay_hints(
+        this: ModelHandle<Self>,
+        _: TypedEnvelope<proto::RefreshInlayHints>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::Ack> {
+        this.update(&mut cx, |_, cx| {
+            cx.emit(Event::RefreshInlays);
+        });
+        Ok(proto::Ack {})
+    }
+
     async fn handle_lsp_command<T: LspCommand>(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<T::ProtoRequest>,
@@ -7316,16 +7506,11 @@ impl Project {
     ) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<LanguageServer>)> {
         self.language_server_ids_for_buffer(buffer, cx)
             .into_iter()
-            .filter_map(|server_id| {
-                let server = self.language_servers.get(&server_id)?;
-                if let LanguageServerState::Running {
+            .filter_map(|server_id| match self.language_servers.get(&server_id)? {
+                LanguageServerState::Running {
                     adapter, server, ..
-                } = server
-                {
-                    Some((adapter, server))
-                } else {
-                    None
-                }
+                } => Some((adapter, server)),
+                _ => None,
             })
     }
 

crates/rope/src/rope.rs 🔗

@@ -384,6 +384,12 @@ impl<'a> From<&'a str> for Rope {
     }
 }
 
+impl From<String> for Rope {
+    fn from(text: String) -> Self {
+        Rope::from(text.as_str())
+    }
+}
+
 impl fmt::Display for Rope {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         for chunk in self.chunks() {

crates/rpc/proto/zed.proto 🔗

@@ -136,6 +136,10 @@ message Envelope {
         OnTypeFormattingResponse on_type_formatting_response = 112;
 
         UpdateWorktreeSettings update_worktree_settings = 113;
+
+        InlayHints inlay_hints = 116;
+        InlayHintsResponse inlay_hints_response = 117;
+        RefreshInlayHints refresh_inlay_hints = 118;
     }
 }
 
@@ -705,6 +709,68 @@ message OnTypeFormattingResponse {
     Transaction transaction = 1;
 }
 
+message InlayHints {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    Anchor start = 3;
+    Anchor end = 4;
+    repeated VectorClockEntry version = 5;
+}
+
+message InlayHintsResponse {
+    repeated InlayHint hints = 1;
+    repeated VectorClockEntry version = 2;
+}
+
+message InlayHint {
+    Anchor position = 1;
+    InlayHintLabel label = 2;
+    optional string kind = 3;
+    bool padding_left = 4;
+    bool padding_right = 5;
+    InlayHintTooltip tooltip = 6;
+}
+
+message InlayHintLabel {
+    oneof label {
+        string value = 1;
+        InlayHintLabelParts label_parts = 2;
+    }
+}
+
+message InlayHintLabelParts {
+    repeated InlayHintLabelPart parts = 1;
+}
+
+message InlayHintLabelPart {
+    string value = 1;
+    InlayHintLabelPartTooltip tooltip = 2;
+    Location location = 3;
+}
+
+message InlayHintTooltip {
+    oneof content {
+        string value = 1;
+        MarkupContent markup_content = 2;
+    }
+}
+
+message InlayHintLabelPartTooltip {
+    oneof content {
+        string value = 1;
+        MarkupContent markup_content = 2;
+    }
+}
+
+message RefreshInlayHints {
+    uint64 project_id = 1;
+}
+
+message MarkupContent {
+    string kind = 1;
+    string value = 2;
+}
+
 message PerformRenameResponse {
     ProjectTransaction transaction = 2;
 }

crates/rpc/src/proto.rs 🔗

@@ -198,6 +198,9 @@ messages!(
     (PerformRenameResponse, Background),
     (OnTypeFormatting, Background),
     (OnTypeFormattingResponse, Background),
+    (InlayHints, Background),
+    (InlayHintsResponse, Background),
+    (RefreshInlayHints, Foreground),
     (Ping, Foreground),
     (PrepareRename, Background),
     (PrepareRenameResponse, Background),
@@ -286,6 +289,8 @@ request_messages!(
     (PerformRename, PerformRenameResponse),
     (PrepareRename, PrepareRenameResponse),
     (OnTypeFormatting, OnTypeFormattingResponse),
+    (InlayHints, InlayHintsResponse),
+    (RefreshInlayHints, Ack),
     (ReloadBuffers, ReloadBuffersResponse),
     (RequestContact, Ack),
     (RemoveContact, Ack),
@@ -332,6 +337,8 @@ entity_messages!(
     OpenBufferForSymbol,
     PerformRename,
     OnTypeFormatting,
+    InlayHints,
+    RefreshInlayHints,
     PrepareRename,
     ReloadBuffers,
     RemoveProjectCollaborator,

crates/sum_tree/src/cursor.rs 🔗

@@ -97,6 +97,42 @@ where
         }
     }
 
+    pub fn next_item(&self) -> Option<&'a T> {
+        self.assert_did_seek();
+        if let Some(entry) = self.stack.last() {
+            if entry.index == entry.tree.0.items().len() - 1 {
+                if let Some(next_leaf) = self.next_leaf() {
+                    Some(next_leaf.0.items().first().unwrap())
+                } else {
+                    None
+                }
+            } else {
+                match *entry.tree.0 {
+                    Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]),
+                    _ => unreachable!(),
+                }
+            }
+        } else if self.at_end {
+            None
+        } else {
+            self.tree.first()
+        }
+    }
+
+    fn next_leaf(&self) -> Option<&'a SumTree<T>> {
+        for entry in self.stack.iter().rev().skip(1) {
+            if entry.index < entry.tree.0.child_trees().len() - 1 {
+                match *entry.tree.0 {
+                    Node::Internal {
+                        ref child_trees, ..
+                    } => return Some(child_trees[entry.index + 1].leftmost_leaf()),
+                    Node::Leaf { .. } => unreachable!(),
+                };
+            }
+        }
+        None
+    }
+
     pub fn prev_item(&self) -> Option<&'a T> {
         self.assert_did_seek();
         if let Some(entry) = self.stack.last() {

crates/sum_tree/src/sum_tree.rs 🔗

@@ -95,31 +95,18 @@ impl<D> fmt::Debug for End<D> {
     }
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)]
 pub enum Bias {
+    #[default]
     Left,
     Right,
 }
 
-impl Default for Bias {
-    fn default() -> Self {
-        Bias::Left
-    }
-}
-
-impl PartialOrd for Bias {
-    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
-impl Ord for Bias {
-    fn cmp(&self, other: &Self) -> Ordering {
-        match (self, other) {
-            (Self::Left, Self::Left) => Ordering::Equal,
-            (Self::Left, Self::Right) => Ordering::Less,
-            (Self::Right, Self::Right) => Ordering::Equal,
-            (Self::Right, Self::Left) => Ordering::Greater,
+impl Bias {
+    pub fn invert(self) -> Self {
+        match self {
+            Self::Left => Self::Right,
+            Self::Right => Self::Left,
         }
     }
 }
@@ -838,6 +825,14 @@ mod tests {
                         assert_eq!(cursor.item(), None);
                     }
 
+                    if before_start {
+                        assert_eq!(cursor.next_item(), reference_items.get(0));
+                    } else if pos + 1 < reference_items.len() {
+                        assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]);
+                    } else {
+                        assert_eq!(cursor.next_item(), None);
+                    }
+
                     if i < 5 {
                         cursor.next(&());
                         if pos < reference_items.len() {
@@ -883,14 +878,17 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
         cursor.prev(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
         cursor.next(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
 
         // Single-element tree
@@ -903,22 +901,26 @@ mod tests {
         );
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
 
         cursor.next(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 1);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 0);
 
         let mut cursor = tree.cursor::<IntegersSummary>();
         assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]);
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 1);
 
         cursor.seek(&Count(0), Bias::Right, &());
@@ -930,6 +932,7 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 1);
 
         // Multiple-element tree
@@ -940,67 +943,80 @@ mod tests {
         assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]);
         assert_eq!(cursor.item(), Some(&3));
         assert_eq!(cursor.prev_item(), Some(&2));
+        assert_eq!(cursor.next_item(), Some(&4));
         assert_eq!(cursor.start().sum, 3);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&4));
         assert_eq!(cursor.prev_item(), Some(&3));
+        assert_eq!(cursor.next_item(), Some(&5));
         assert_eq!(cursor.start().sum, 6);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&5));
         assert_eq!(cursor.prev_item(), Some(&4));
+        assert_eq!(cursor.next_item(), Some(&6));
         assert_eq!(cursor.start().sum, 10);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&6));
         assert_eq!(cursor.prev_item(), Some(&5));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 15);
 
         cursor.next(&());
         cursor.next(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&6));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 21);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&6));
         assert_eq!(cursor.prev_item(), Some(&5));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 15);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&5));
         assert_eq!(cursor.prev_item(), Some(&4));
+        assert_eq!(cursor.next_item(), Some(&6));
         assert_eq!(cursor.start().sum, 10);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&4));
         assert_eq!(cursor.prev_item(), Some(&3));
+        assert_eq!(cursor.next_item(), Some(&5));
         assert_eq!(cursor.start().sum, 6);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&3));
         assert_eq!(cursor.prev_item(), Some(&2));
+        assert_eq!(cursor.next_item(), Some(&4));
         assert_eq!(cursor.start().sum, 3);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&2));
         assert_eq!(cursor.prev_item(), Some(&1));
+        assert_eq!(cursor.next_item(), Some(&3));
         assert_eq!(cursor.start().sum, 1);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), Some(&2));
         assert_eq!(cursor.start().sum, 0);
 
         cursor.prev(&());
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), Some(&1));
         assert_eq!(cursor.start().sum, 0);
 
         cursor.next(&());
         assert_eq!(cursor.item(), Some(&1));
         assert_eq!(cursor.prev_item(), None);
+        assert_eq!(cursor.next_item(), Some(&2));
         assert_eq!(cursor.start().sum, 0);
 
         let mut cursor = tree.cursor::<IntegersSummary>();
@@ -1012,6 +1028,7 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&6));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 21);
 
         cursor.seek(&Count(3), Bias::Right, &());
@@ -1023,6 +1040,7 @@ mod tests {
         );
         assert_eq!(cursor.item(), None);
         assert_eq!(cursor.prev_item(), Some(&6));
+        assert_eq!(cursor.next_item(), None);
         assert_eq!(cursor.start().sum, 21);
 
         // Seeking can bias left or right

crates/theme/src/theme.rs 🔗

@@ -689,6 +689,7 @@ pub struct Editor {
     pub line_number_active: Color,
     pub guest_selections: Vec<SelectionStyle>,
     pub syntax: Arc<SyntaxTheme>,
+    pub hint: HighlightStyle,
     pub suggestion: HighlightStyle,
     pub diagnostic_path_header: DiagnosticPathHeader,
     pub diagnostic_header: DiagnosticHeader,

styles/src/style_tree/editor.ts 🔗

@@ -53,6 +53,7 @@ export default function editor(theme: ColorScheme): any {
         active_line_background: with_opacity(background(layer, "on"), 0.75),
         highlighted_line_background: background(layer, "on"),
         // Inline autocomplete suggestions, Co-pilot suggestions, etc.
+        hint: syntax.hint,
         suggestion: syntax.predictive,
         code_actions: {
             indicator: toggleable({

styles/src/theme/syntax.ts 🔗

@@ -17,6 +17,7 @@ export interface Syntax {
     "comment.doc": SyntaxHighlightStyle
     primary: SyntaxHighlightStyle
     predictive: SyntaxHighlightStyle
+    hint: SyntaxHighlightStyle
 
     // === Formatted Text ====== /
     emphasis: SyntaxHighlightStyle
@@ -146,12 +147,23 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
             "lch"
         )
         .hex()
+    // Mix the neutral and green colors to get a
+    // hint color distinct from any other color in the theme
+    const hint = chroma
+        .mix(
+            color_scheme.ramps.neutral(0.6).hex(),
+            color_scheme.ramps.blue(0.4).hex(),
+            0.45,
+            "lch"
+        )
+        .hex()
 
     const color = {
         primary: color_scheme.ramps.neutral(1).hex(),
         comment: color_scheme.ramps.neutral(0.71).hex(),
         punctuation: color_scheme.ramps.neutral(0.86).hex(),
         predictive: predictive,
+        hint: hint,
         emphasis: color_scheme.ramps.blue(0.5).hex(),
         string: color_scheme.ramps.orange(0.5).hex(),
         function: color_scheme.ramps.yellow(0.5).hex(),
@@ -183,6 +195,10 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
             color: color.predictive,
             italic: true,
         },
+        hint: {
+            color: color.hint,
+            weight: font_weights.bold,
+        },
         emphasis: {
             color: color.emphasis,
         },