Refactor LSP-related logic (#48604)

Kirill Bulatov created

* split more logic away into its own modules for document colors, code
lens and inlay hints
* remove unnecessary cache_version for document colors

Release Notes:

- N/A

Change summary

crates/editor/src/editor.rs                     |  12 
crates/editor/src/editor_tests.rs               | 335 --------
crates/editor/src/lsp_colors.rs                 | 413 +++++++++-
crates/project/src/lsp_store.rs                 | 715 ------------------
crates/project/src/lsp_store/code_lens.rs       | 223 +++++
crates/project/src/lsp_store/document_colors.rs | 401 ++++++++++
crates/project/src/lsp_store/inlay_hints.rs     | 118 +++
crates/settings_ui/src/page_data.rs             |   2 
8 files changed, 1,139 insertions(+), 1,080 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2159,13 +2159,13 @@ impl Editor {
                         );
                     }
                     project::Event::LanguageServerRemoved(_server_id) => {
-                        if editor.tasks_update_task.is_none() {
-                            editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
-                        }
                         editor.registered_buffers.clear();
                         editor.register_visible_buffers(cx);
-                        editor.refresh_semantic_token_highlights(cx);
+                        editor.update_lsp_data(None, window, cx);
                         editor.refresh_inlay_hints(InlayHintRefreshReason::ServerRemoved, cx);
+                        if editor.tasks_update_task.is_none() {
+                            editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
+                        }
                     }
                     project::Event::LanguageServerAdded(..) => {
                         if editor.tasks_update_task.is_none() {
@@ -25209,9 +25209,11 @@ impl Editor {
     ) {
         if let Some(buffer_id) = for_buffer {
             self.pull_diagnostics(buffer_id, window, cx);
+            self.update_semantic_tokens(Some(buffer_id), None, cx);
+        } else {
+            self.refresh_semantic_token_highlights(cx);
         }
         self.refresh_colors_for_visible_range(for_buffer, window, cx);
-        self.update_semantic_tokens(for_buffer, None, cx);
     }
 
     fn register_visible_buffers(&mut self, cx: &mut Context<Self>) {

crates/editor/src/editor_tests.rs 🔗

@@ -17,7 +17,7 @@ use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkS
 use collections::HashMap;
 use futures::{StreamExt, channel::oneshot};
 use gpui::{
-    BackgroundExecutor, DismissEvent, Rgba, TestAppContext, UpdateGlobal, VisualTestContext,
+    BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext,
     WindowBounds, WindowOptions, div,
 };
 use indoc::indoc;
@@ -67,8 +67,7 @@ use util::{
     uri,
 };
 use workspace::{
-    CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
-    OpenOptions, ViewId,
+    CloseActiveItem, CloseAllItems, CloseOtherItems, NavigationEntry, OpenOptions, ViewId,
     item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
     register_project_item,
 };
@@ -27964,326 +27963,6 @@ async fn test_insert_snippet(cx: &mut TestAppContext) {
     );
 }
 
-#[gpui::test(iterations = 10)]
-async fn test_document_colors(cx: &mut TestAppContext) {
-    let expected_color = Rgba {
-        r: 0.33,
-        g: 0.33,
-        b: 0.33,
-        a: 0.33,
-    };
-
-    init_test(cx, |_| {});
-
-    let fs = FakeFs::new(cx.executor());
-    fs.insert_tree(
-        path!("/a"),
-        json!({
-            "first.rs": "fn main() { let a = 5; }",
-        }),
-    )
-    .await;
-
-    let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
-    let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
-    let cx = &mut VisualTestContext::from_window(*workspace, cx);
-
-    let language_registry = project.read_with(cx, |project, _| project.languages().clone());
-    language_registry.add(rust_lang());
-    let mut fake_servers = language_registry.register_fake_lsp(
-        "Rust",
-        FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
-                ..lsp::ServerCapabilities::default()
-            },
-            name: "rust-analyzer",
-            ..FakeLspAdapter::default()
-        },
-    );
-    let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
-        "Rust",
-        FakeLspAdapter {
-            capabilities: lsp::ServerCapabilities {
-                color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
-                ..lsp::ServerCapabilities::default()
-            },
-            name: "not-rust-analyzer",
-            ..FakeLspAdapter::default()
-        },
-    );
-
-    let editor = workspace
-        .update(cx, |workspace, window, cx| {
-            workspace.open_abs_path(
-                PathBuf::from(path!("/a/first.rs")),
-                OpenOptions::default(),
-                window,
-                cx,
-            )
-        })
-        .unwrap()
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-    let fake_language_server = fake_servers.next().await.unwrap();
-    let fake_language_server_without_capabilities =
-        fake_servers_without_capabilities.next().await.unwrap();
-    let requests_made = Arc::new(AtomicUsize::new(0));
-    let closure_requests_made = Arc::clone(&requests_made);
-    let mut color_request_handle = fake_language_server
-        .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
-            let requests_made = Arc::clone(&closure_requests_made);
-            async move {
-                assert_eq!(
-                    params.text_document.uri,
-                    lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
-                );
-                requests_made.fetch_add(1, atomic::Ordering::Release);
-                Ok(vec![
-                    lsp::ColorInformation {
-                        range: lsp::Range {
-                            start: lsp::Position {
-                                line: 0,
-                                character: 0,
-                            },
-                            end: lsp::Position {
-                                line: 0,
-                                character: 1,
-                            },
-                        },
-                        color: lsp::Color {
-                            red: 0.33,
-                            green: 0.33,
-                            blue: 0.33,
-                            alpha: 0.33,
-                        },
-                    },
-                    lsp::ColorInformation {
-                        range: lsp::Range {
-                            start: lsp::Position {
-                                line: 0,
-                                character: 0,
-                            },
-                            end: lsp::Position {
-                                line: 0,
-                                character: 1,
-                            },
-                        },
-                        color: lsp::Color {
-                            red: 0.33,
-                            green: 0.33,
-                            blue: 0.33,
-                            alpha: 0.33,
-                        },
-                    },
-                ])
-            }
-        });
-
-    let _handle = fake_language_server_without_capabilities
-        .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
-            panic!("Should not be called");
-        });
-    cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
-    color_request_handle.next().await.unwrap();
-    cx.run_until_parked();
-    assert_eq!(
-        1,
-        requests_made.load(atomic::Ordering::Acquire),
-        "Should query for colors once per editor open"
-    );
-    editor.update_in(cx, |editor, _, cx| {
-        assert_eq!(
-            vec![expected_color],
-            extract_color_inlays(editor, cx),
-            "Should have an initial inlay"
-        );
-    });
-
-    // opening another file in a split should not influence the LSP query counter
-    workspace
-        .update(cx, |workspace, window, cx| {
-            assert_eq!(
-                workspace.panes().len(),
-                1,
-                "Should have one pane with one editor"
-            );
-            workspace.move_item_to_pane_in_direction(
-                &MoveItemToPaneInDirection {
-                    direction: SplitDirection::Right,
-                    focus: false,
-                    clone: true,
-                },
-                window,
-                cx,
-            );
-        })
-        .unwrap();
-    cx.run_until_parked();
-    workspace
-        .update(cx, |workspace, _, cx| {
-            let panes = workspace.panes();
-            assert_eq!(panes.len(), 2, "Should have two panes after splitting");
-            for pane in panes {
-                let editor = pane
-                    .read(cx)
-                    .active_item()
-                    .and_then(|item| item.downcast::<Editor>())
-                    .expect("Should have opened an editor in each split");
-                let editor_file = editor
-                    .read(cx)
-                    .buffer()
-                    .read(cx)
-                    .as_singleton()
-                    .expect("test deals with singleton buffers")
-                    .read(cx)
-                    .file()
-                    .expect("test buffese should have a file")
-                    .path();
-                assert_eq!(
-                    editor_file.as_ref(),
-                    rel_path("first.rs"),
-                    "Both editors should be opened for the same file"
-                )
-            }
-        })
-        .unwrap();
-
-    cx.executor().advance_clock(Duration::from_millis(500));
-    let save = editor.update_in(cx, |editor, window, cx| {
-        editor.move_to_end(&MoveToEnd, window, cx);
-        editor.handle_input("dirty", window, cx);
-        editor.save(
-            SaveOptions {
-                format: true,
-                autosave: true,
-            },
-            project.clone(),
-            window,
-            cx,
-        )
-    });
-    save.await.unwrap();
-
-    color_request_handle.next().await.unwrap();
-    cx.run_until_parked();
-    assert_eq!(
-        2,
-        requests_made.load(atomic::Ordering::Acquire),
-        "Should query for colors once per save (deduplicated) and once per formatting after save"
-    );
-
-    drop(editor);
-    let close = workspace
-        .update(cx, |workspace, window, cx| {
-            workspace.active_pane().update(cx, |pane, cx| {
-                pane.close_active_item(&CloseActiveItem::default(), window, cx)
-            })
-        })
-        .unwrap();
-    close.await.unwrap();
-    let close = workspace
-        .update(cx, |workspace, window, cx| {
-            workspace.active_pane().update(cx, |pane, cx| {
-                pane.close_active_item(&CloseActiveItem::default(), window, cx)
-            })
-        })
-        .unwrap();
-    close.await.unwrap();
-    assert_eq!(
-        2,
-        requests_made.load(atomic::Ordering::Acquire),
-        "After saving and closing all editors, no extra requests should be made"
-    );
-    workspace
-        .update(cx, |workspace, _, cx| {
-            assert!(
-                workspace.active_item(cx).is_none(),
-                "Should close all editors"
-            )
-        })
-        .unwrap();
-
-    workspace
-        .update(cx, |workspace, window, cx| {
-            workspace.active_pane().update(cx, |pane, cx| {
-                pane.navigate_backward(&workspace::GoBack, window, cx);
-            })
-        })
-        .unwrap();
-    cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
-    cx.run_until_parked();
-    let editor = workspace
-        .update(cx, |workspace, _, cx| {
-            workspace
-                .active_item(cx)
-                .expect("Should have reopened the editor again after navigating back")
-                .downcast::<Editor>()
-                .expect("Should be an editor")
-        })
-        .unwrap();
-
-    assert_eq!(
-        2,
-        requests_made.load(atomic::Ordering::Acquire),
-        "Cache should be reused on buffer close and reopen"
-    );
-    editor.update(cx, |editor, cx| {
-        assert_eq!(
-            vec![expected_color],
-            extract_color_inlays(editor, cx),
-            "Should have an initial inlay"
-        );
-    });
-
-    drop(color_request_handle);
-    let closure_requests_made = Arc::clone(&requests_made);
-    let mut empty_color_request_handle = fake_language_server
-        .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
-            let requests_made = Arc::clone(&closure_requests_made);
-            async move {
-                assert_eq!(
-                    params.text_document.uri,
-                    lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
-                );
-                requests_made.fetch_add(1, atomic::Ordering::Release);
-                Ok(Vec::new())
-            }
-        });
-    let save = editor.update_in(cx, |editor, window, cx| {
-        editor.move_to_end(&MoveToEnd, window, cx);
-        editor.handle_input("dirty_again", window, cx);
-        editor.save(
-            SaveOptions {
-                format: false,
-                autosave: true,
-            },
-            project.clone(),
-            window,
-            cx,
-        )
-    });
-    save.await.unwrap();
-
-    cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
-    empty_color_request_handle.next().await.unwrap();
-    cx.run_until_parked();
-    assert_eq!(
-        3,
-        requests_made.load(atomic::Ordering::Acquire),
-        "Should query for colors once per save only, as formatting was not requested"
-    );
-    editor.update(cx, |editor, cx| {
-        assert_eq!(
-            Vec::<Rgba>::new(),
-            extract_color_inlays(editor, cx),
-            "Should clear all colors when the server returns an empty response"
-        );
-    });
-}
-
 #[gpui::test]
 async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
@@ -28988,16 +28667,6 @@ async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
     cx.update(|_, cx| assert!(editor.is_dirty(cx)));
 }
 
-#[track_caller]
-fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
-    editor
-        .all_inlays(cx)
-        .into_iter()
-        .filter_map(|inlay| inlay.get_color())
-        .map(Rgba::from)
-        .collect()
-}
-
 #[gpui::test]
 fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/lsp_colors.rs 🔗

@@ -27,7 +27,6 @@ pub(super) struct LspColorData {
 struct BufferColors {
     colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
     inlay_colors: HashMap<InlayId, usize>,
-    cache_version_used: usize,
 }
 
 impl LspColorData {
@@ -94,12 +93,8 @@ impl LspColorData {
         &mut self,
         buffer_id: BufferId,
         colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
-        cache_version: Option<usize>,
     ) -> bool {
         let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
-        if let Some(cache_version) = cache_version {
-            buffer_colors.cache_version_used = cache_version;
-        }
         if buffer_colors.colors == colors {
             return false;
         }
@@ -152,7 +147,7 @@ impl Editor {
         if !self.mode().is_full() {
             return;
         }
-        let Some(project) = self.project.clone() else {
+        let Some(project) = self.project.as_ref() else {
             return;
         };
         if self
@@ -176,29 +171,20 @@ impl Editor {
             .unique_by(|buffer| buffer.read(cx).remote_id())
             .collect::<Vec<_>>();
 
+        let project = project.downgrade();
         self.refresh_colors_task = cx.spawn(async move |editor, cx| {
             cx.background_executor()
                 .timer(FETCH_COLORS_DEBOUNCE_TIMEOUT)
                 .await;
 
-            let Some(all_colors_task) = editor
-                .update(cx, |editor, cx| {
-                    project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
+            let Some(all_colors_task) = project
+                .update(cx, |project, cx| {
+                    project.lsp_store().update(cx, |lsp_store, cx| {
                         buffers_to_query
                             .into_iter()
                             .filter_map(|buffer| {
                                 let buffer_id = buffer.read(cx).remote_id();
-                                let known_cache_version =
-                                    editor.colors.as_ref().and_then(|colors| {
-                                        Some(
-                                            colors
-                                                .buffer_colors
-                                                .get(&buffer_id)?
-                                                .cache_version_used,
-                                        )
-                                    });
-                                let colors_task =
-                                    lsp_store.document_colors(known_cache_version, buffer, cx)?;
+                                let colors_task = lsp_store.document_colors(buffer, cx)?;
                                 Some(async move { (buffer_id, colors_task.await) })
                             })
                             .collect::<Vec<_>>()
@@ -236,7 +222,8 @@ impl Editor {
                 return;
             };
 
-            let mut new_editor_colors = HashMap::default();
+            let mut new_editor_colors: HashMap<BufferId, Vec<(Range<Anchor>, DocumentColor)>> =
+                HashMap::default();
             for (buffer_id, colors) in all_colors {
                 let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
                     continue;
@@ -244,12 +231,10 @@ impl Editor {
                 match colors {
                     Ok(colors) => {
                         if colors.colors.is_empty() {
-                            let new_entry =
-                                new_editor_colors.entry(buffer_id).or_insert_with(|| {
-                                    (Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
-                                });
-                            new_entry.0.clear();
-                            new_entry.1 = colors.cache_version;
+                            new_editor_colors
+                                .entry(buffer_id)
+                                .or_insert_with(Vec::new)
+                                .clear();
                         } else {
                             for color in colors.colors {
                                 let color_start = point_from_lsp(color.lsp_range.start);
@@ -273,12 +258,8 @@ impl Editor {
                                         continue;
                                     };
 
-                                    let new_entry =
-                                        new_editor_colors.entry(buffer_id).or_insert_with(|| {
-                                            (Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
-                                        });
-                                    new_entry.1 = colors.cache_version;
-                                    let new_buffer_colors = &mut new_entry.0;
+                                    let new_buffer_colors =
+                                        new_editor_colors.entry(buffer_id).or_insert_with(Vec::new);
 
                                     let (Ok(i) | Err(i)) =
                                         new_buffer_colors.binary_search_by(|(probe, _)| {
@@ -308,7 +289,7 @@ impl Editor {
                         return;
                     };
                     let mut updated = false;
-                    for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
+                    for (buffer_id, new_buffer_colors) in new_editor_colors {
                         let mut new_buffer_color_inlays =
                             Vec::with_capacity(new_buffer_colors.len());
                         let mut existing_buffer_colors = colors
@@ -402,11 +383,7 @@ impl Editor {
                                 .to_remove
                                 .extend(existing_buffer_colors.map(|(_, _, id)| *id));
                         }
-                        updated |= colors.set_colors(
-                            buffer_id,
-                            new_buffer_color_inlays,
-                            new_cache_version,
-                        );
+                        updated |= colors.set_colors(buffer_id, new_buffer_color_inlays);
                     }
 
                     if colors.render_mode == DocumentColorsRenderMode::Inlay
@@ -424,3 +401,361 @@ impl Editor {
         });
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::{
+        path::PathBuf,
+        sync::{
+            Arc,
+            atomic::{self, AtomicUsize},
+        },
+        time::Duration,
+    };
+
+    use futures::StreamExt;
+    use gpui::{Rgba, TestAppContext, VisualTestContext};
+    use language::FakeLspAdapter;
+    use languages::rust_lang;
+    use project::{FakeFs, Project};
+    use serde_json::json;
+    use util::{path, rel_path::rel_path};
+    use workspace::{
+        CloseActiveItem, MoveItemToPaneInDirection, OpenOptions,
+        item::{Item as _, SaveOptions},
+    };
+
+    use crate::{
+        Editor, FETCH_COLORS_DEBOUNCE_TIMEOUT, actions::MoveToEnd, editor_tests::init_test,
+    };
+
+    fn extract_color_inlays(editor: &Editor, cx: &gpui::App) -> Vec<Rgba> {
+        editor
+            .all_inlays(cx)
+            .into_iter()
+            .filter_map(|inlay| inlay.get_color())
+            .map(Rgba::from)
+            .collect()
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_document_colors(cx: &mut TestAppContext) {
+        let expected_color = Rgba {
+            r: 0.33,
+            g: 0.33,
+            b: 0.33,
+            a: 0.33,
+        };
+
+        init_test(cx, |_| {});
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/a"),
+            json!({
+                "first.rs": "fn main() { let a = 5; }",
+            }),
+        )
+        .await;
+
+        let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
+        let workspace =
+            cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx));
+        let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+        let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+        language_registry.add(rust_lang());
+        let mut fake_servers = language_registry.register_fake_lsp(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
+                    ..lsp::ServerCapabilities::default()
+                },
+                name: "rust-analyzer",
+                ..FakeLspAdapter::default()
+            },
+        );
+        let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
+            "Rust",
+            FakeLspAdapter {
+                capabilities: lsp::ServerCapabilities {
+                    color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
+                    ..lsp::ServerCapabilities::default()
+                },
+                name: "not-rust-analyzer",
+                ..FakeLspAdapter::default()
+            },
+        );
+
+        let editor = workspace
+            .update(cx, |workspace, window, cx| {
+                workspace.open_abs_path(
+                    PathBuf::from(path!("/a/first.rs")),
+                    OpenOptions::default(),
+                    window,
+                    cx,
+                )
+            })
+            .unwrap()
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let fake_language_server = fake_servers.next().await.unwrap();
+        let fake_language_server_without_capabilities =
+            fake_servers_without_capabilities.next().await.unwrap();
+        let requests_made = Arc::new(AtomicUsize::new(0));
+        let closure_requests_made = Arc::clone(&requests_made);
+        let mut color_request_handle = fake_language_server
+            .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
+                let requests_made = Arc::clone(&closure_requests_made);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
+                    );
+                    requests_made.fetch_add(1, atomic::Ordering::Release);
+                    Ok(vec![
+                        lsp::ColorInformation {
+                            range: lsp::Range {
+                                start: lsp::Position {
+                                    line: 0,
+                                    character: 0,
+                                },
+                                end: lsp::Position {
+                                    line: 0,
+                                    character: 1,
+                                },
+                            },
+                            color: lsp::Color {
+                                red: 0.33,
+                                green: 0.33,
+                                blue: 0.33,
+                                alpha: 0.33,
+                            },
+                        },
+                        lsp::ColorInformation {
+                            range: lsp::Range {
+                                start: lsp::Position {
+                                    line: 0,
+                                    character: 0,
+                                },
+                                end: lsp::Position {
+                                    line: 0,
+                                    character: 1,
+                                },
+                            },
+                            color: lsp::Color {
+                                red: 0.33,
+                                green: 0.33,
+                                blue: 0.33,
+                                alpha: 0.33,
+                            },
+                        },
+                    ])
+                }
+            });
+
+        let _handle = fake_language_server_without_capabilities
+            .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
+                panic!("Should not be called");
+            });
+        cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
+        color_request_handle.next().await.unwrap();
+        cx.run_until_parked();
+        assert_eq!(
+            1,
+            requests_made.load(atomic::Ordering::Acquire),
+            "Should query for colors once per editor open"
+        );
+        editor.update_in(cx, |editor, _, cx| {
+            assert_eq!(
+                vec![expected_color],
+                extract_color_inlays(editor, cx),
+                "Should have an initial inlay"
+            );
+        });
+
+        // opening another file in a split should not influence the LSP query counter
+        workspace
+            .update(cx, |workspace, window, cx| {
+                assert_eq!(
+                    workspace.panes().len(),
+                    1,
+                    "Should have one pane with one editor"
+                );
+                workspace.move_item_to_pane_in_direction(
+                    &MoveItemToPaneInDirection {
+                        direction: workspace::SplitDirection::Right,
+                        focus: false,
+                        clone: true,
+                    },
+                    window,
+                    cx,
+                );
+            })
+            .unwrap();
+        cx.run_until_parked();
+        workspace
+            .update(cx, |workspace, _, cx| {
+                let panes = workspace.panes();
+                assert_eq!(panes.len(), 2, "Should have two panes after splitting");
+                for pane in panes {
+                    let editor = pane
+                        .read(cx)
+                        .active_item()
+                        .and_then(|item| item.downcast::<Editor>())
+                        .expect("Should have opened an editor in each split");
+                    let editor_file = editor
+                        .read(cx)
+                        .buffer()
+                        .read(cx)
+                        .as_singleton()
+                        .expect("test deals with singleton buffers")
+                        .read(cx)
+                        .file()
+                        .expect("test buffese should have a file")
+                        .path();
+                    assert_eq!(
+                        editor_file.as_ref(),
+                        rel_path("first.rs"),
+                        "Both editors should be opened for the same file"
+                    )
+                }
+            })
+            .unwrap();
+
+        cx.executor().advance_clock(Duration::from_millis(500));
+        let save = editor.update_in(cx, |editor, window, cx| {
+            editor.move_to_end(&MoveToEnd, window, cx);
+            editor.handle_input("dirty", window, cx);
+            editor.save(
+                SaveOptions {
+                    format: true,
+                    autosave: true,
+                },
+                project.clone(),
+                window,
+                cx,
+            )
+        });
+        save.await.unwrap();
+
+        color_request_handle.next().await.unwrap();
+        cx.run_until_parked();
+        assert_eq!(
+            2,
+            requests_made.load(atomic::Ordering::Acquire),
+            "Should query for colors once per save (deduplicated) and once per formatting after save"
+        );
+
+        drop(editor);
+        let close = workspace
+            .update(cx, |workspace, window, cx| {
+                workspace.active_pane().update(cx, |pane, cx| {
+                    pane.close_active_item(&CloseActiveItem::default(), window, cx)
+                })
+            })
+            .unwrap();
+        close.await.unwrap();
+        let close = workspace
+            .update(cx, |workspace, window, cx| {
+                workspace.active_pane().update(cx, |pane, cx| {
+                    pane.close_active_item(&CloseActiveItem::default(), window, cx)
+                })
+            })
+            .unwrap();
+        close.await.unwrap();
+        assert_eq!(
+            2,
+            requests_made.load(atomic::Ordering::Acquire),
+            "After saving and closing all editors, no extra requests should be made"
+        );
+        workspace
+            .update(cx, |workspace, _, cx| {
+                assert!(
+                    workspace.active_item(cx).is_none(),
+                    "Should close all editors"
+                )
+            })
+            .unwrap();
+
+        workspace
+            .update(cx, |workspace, window, cx| {
+                workspace.active_pane().update(cx, |pane, cx| {
+                    pane.navigate_backward(&workspace::GoBack, window, cx);
+                })
+            })
+            .unwrap();
+        cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
+        cx.run_until_parked();
+        let editor = workspace
+            .update(cx, |workspace, _, cx| {
+                workspace
+                    .active_item(cx)
+                    .expect("Should have reopened the editor again after navigating back")
+                    .downcast::<Editor>()
+                    .expect("Should be an editor")
+            })
+            .unwrap();
+
+        assert_eq!(
+            2,
+            requests_made.load(atomic::Ordering::Acquire),
+            "Cache should be reused on buffer close and reopen"
+        );
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                vec![expected_color],
+                extract_color_inlays(editor, cx),
+                "Should have an initial inlay"
+            );
+        });
+
+        drop(color_request_handle);
+        let closure_requests_made = Arc::clone(&requests_made);
+        let mut empty_color_request_handle = fake_language_server
+            .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
+                let requests_made = Arc::clone(&closure_requests_made);
+                async move {
+                    assert_eq!(
+                        params.text_document.uri,
+                        lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
+                    );
+                    requests_made.fetch_add(1, atomic::Ordering::Release);
+                    Ok(Vec::new())
+                }
+            });
+        let save = editor.update_in(cx, |editor, window, cx| {
+            editor.move_to_end(&MoveToEnd, window, cx);
+            editor.handle_input("dirty_again", window, cx);
+            editor.save(
+                SaveOptions {
+                    format: false,
+                    autosave: true,
+                },
+                project.clone(),
+                window,
+                cx,
+            )
+        });
+        save.await.unwrap();
+
+        cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
+        empty_color_request_handle.next().await.unwrap();
+        cx.run_until_parked();
+        assert_eq!(
+            3,
+            requests_made.load(atomic::Ordering::Acquire),
+            "Should query for colors once per save only, as formatting was not requested"
+        );
+        editor.update(cx, |editor, cx| {
+            assert_eq!(
+                Vec::<Rgba>::new(),
+                extract_color_inlays(editor, cx),
+                "Should clear all colors when the server returns an empty response"
+            );
+        });
+    }
+}

crates/project/src/lsp_store.rs 🔗

@@ -10,6 +10,8 @@
 //!
 //! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate.
 pub mod clangd_ext;
+mod code_lens;
+mod document_colors;
 pub mod json_language_server_ext;
 pub mod log_store;
 pub mod lsp_ext_command;
@@ -17,14 +19,16 @@ pub mod rust_analyzer_ext;
 mod semantic_tokens;
 pub mod vue_language_server_ext;
 
-mod inlay_hint_cache;
+mod inlay_hints;
 
-use self::inlay_hint_cache::BufferInlayHints;
+use self::code_lens::CodeLensData;
+use self::document_colors::DocumentColorData;
+use self::inlay_hints::BufferInlayHints;
 use crate::{
-    CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse,
-    CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, InlayId, LocationLink,
-    LspAction, LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath,
-    ProjectTransaction, PulledDiagnostics, ResolveState, Symbol,
+    CodeAction, Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource,
+    CoreCompletion, Hover, InlayHint, InlayId, LocationLink, LspAction, LspPullDiagnostics,
+    ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction,
+    PulledDiagnostics, ResolveState, Symbol,
     buffer_store::{BufferStore, BufferStoreEvent},
     environment::ProjectEnvironment,
     lsp_command::{self, *},
@@ -74,8 +78,8 @@ use language::{
     },
     point_to_lsp,
     proto::{
-        deserialize_anchor, deserialize_anchor_range, deserialize_lsp_edit, deserialize_version,
-        serialize_anchor, serialize_anchor_range, serialize_lsp_edit, serialize_version,
+        deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
+        serialize_anchor_range, serialize_version,
     },
     range_from_lsp, range_to_lsp,
     row_chunk::RowChunk,
@@ -136,9 +140,10 @@ use util::{
     rel_path::RelPath,
 };
 
+pub use document_colors::DocumentColors;
 pub use fs::*;
 pub use language::Location;
-pub use lsp_store::inlay_hint_cache::{CacheInlayHints, InvalidationStrategy};
+pub use lsp_store::inlay_hints::{CacheInlayHints, InvalidationStrategy};
 #[cfg(any(test, feature = "test-support"))]
 pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
 pub use semantic_tokens::{
@@ -3878,12 +3883,11 @@ impl BufferLspData {
 
     fn remove_server_data(&mut self, for_server: LanguageServerId) {
         if let Some(document_colors) = &mut self.document_colors {
-            document_colors.colors.remove(&for_server);
-            document_colors.cache_version += 1;
+            document_colors.remove_server_data(for_server);
         }
 
         if let Some(code_lens) = &mut self.code_lens {
-            code_lens.lens.remove(&for_server);
+            code_lens.remove_server_data(for_server);
         }
 
         self.inlay_hints.remove_server_data(for_server);
@@ -3902,28 +3906,6 @@ impl BufferLspData {
     }
 }
 
-#[derive(Debug, Default, Clone)]
-pub struct DocumentColors {
-    pub colors: HashSet<DocumentColor>,
-    pub cache_version: Option<usize>,
-}
-
-type DocumentColorTask = Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>;
-type CodeLensTask = Shared<Task<std::result::Result<Option<Vec<CodeAction>>, Arc<anyhow::Error>>>>;
-
-#[derive(Debug, Default)]
-struct DocumentColorData {
-    colors: HashMap<LanguageServerId, HashSet<DocumentColor>>,
-    cache_version: usize,
-    colors_update: Option<(Global, DocumentColorTask)>,
-}
-
-#[derive(Debug, Default)]
-struct CodeLensData {
-    lens: HashMap<LanguageServerId, Vec<CodeAction>>,
-    update: Option<(Global, CodeLensTask)>,
-}
-
 #[derive(Debug)]
 pub enum LspStoreEvent {
     LanguageServerAdded(LanguageServerId, LanguageServerName, Option<WorktreeId>),
@@ -5428,168 +5410,6 @@ impl LspStore {
         None
     }
 
-    fn resolve_inlay_hint(
-        &self,
-        mut hint: InlayHint,
-        buffer: Entity<Buffer>,
-        server_id: LanguageServerId,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<InlayHint>> {
-        if let Some((upstream_client, project_id)) = self.upstream_client() {
-            if !self.check_if_capable_for_proto_request(&buffer, InlayHints::can_resolve_inlays, cx)
-            {
-                hint.resolve_state = ResolveState::Resolved;
-                return Task::ready(Ok(hint));
-            }
-            let request = proto::ResolveInlayHint {
-                project_id,
-                buffer_id: buffer.read(cx).remote_id().into(),
-                language_server_id: server_id.0 as u64,
-                hint: Some(InlayHints::project_to_proto_hint(hint.clone())),
-            };
-            cx.background_spawn(async move {
-                let response = upstream_client
-                    .request(request)
-                    .await
-                    .context("inlay hints proto request")?;
-                match response.hint {
-                    Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint)
-                        .context("inlay hints proto resolve response conversion"),
-                    None => Ok(hint),
-                }
-            })
-        } else {
-            let Some(lang_server) = buffer.update(cx, |buffer, cx| {
-                self.language_server_for_local_buffer(buffer, server_id, cx)
-                    .map(|(_, server)| server.clone())
-            }) else {
-                return Task::ready(Ok(hint));
-            };
-            if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) {
-                return Task::ready(Ok(hint));
-            }
-            let buffer_snapshot = buffer.read(cx).snapshot();
-            cx.spawn(async move |_, cx| {
-                let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
-                    InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
-                );
-                let resolved_hint = resolve_task
-                    .await
-                    .into_response()
-                    .context("inlay hint resolve LSP request")?;
-                let resolved_hint = InlayHints::lsp_to_project_hint(
-                    resolved_hint,
-                    &buffer,
-                    server_id,
-                    ResolveState::Resolved,
-                    false,
-                    cx,
-                )
-                .await?;
-                Ok(resolved_hint)
-            })
-        }
-    }
-
-    pub fn resolve_color_presentation(
-        &mut self,
-        mut color: DocumentColor,
-        buffer: Entity<Buffer>,
-        server_id: LanguageServerId,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<DocumentColor>> {
-        if color.resolved {
-            return Task::ready(Ok(color));
-        }
-
-        if let Some((upstream_client, project_id)) = self.upstream_client() {
-            let start = color.lsp_range.start;
-            let end = color.lsp_range.end;
-            let request = proto::GetColorPresentation {
-                project_id,
-                server_id: server_id.to_proto(),
-                buffer_id: buffer.read(cx).remote_id().into(),
-                color: Some(proto::ColorInformation {
-                    red: color.color.red,
-                    green: color.color.green,
-                    blue: color.color.blue,
-                    alpha: color.color.alpha,
-                    lsp_range_start: Some(proto::PointUtf16 {
-                        row: start.line,
-                        column: start.character,
-                    }),
-                    lsp_range_end: Some(proto::PointUtf16 {
-                        row: end.line,
-                        column: end.character,
-                    }),
-                }),
-            };
-            cx.background_spawn(async move {
-                let response = upstream_client
-                    .request(request)
-                    .await
-                    .context("color presentation proto request")?;
-                color.resolved = true;
-                color.color_presentations = response
-                    .presentations
-                    .into_iter()
-                    .map(|presentation| ColorPresentation {
-                        label: SharedString::from(presentation.label),
-                        text_edit: presentation.text_edit.and_then(deserialize_lsp_edit),
-                        additional_text_edits: presentation
-                            .additional_text_edits
-                            .into_iter()
-                            .filter_map(deserialize_lsp_edit)
-                            .collect(),
-                    })
-                    .collect();
-                Ok(color)
-            })
-        } else {
-            let path = match buffer
-                .update(cx, |buffer, cx| {
-                    Some(File::from_dyn(buffer.file())?.abs_path(cx))
-                })
-                .context("buffer with the missing path")
-            {
-                Ok(path) => path,
-                Err(e) => return Task::ready(Err(e)),
-            };
-            let Some(lang_server) = buffer.update(cx, |buffer, cx| {
-                self.language_server_for_local_buffer(buffer, server_id, cx)
-                    .map(|(_, server)| server.clone())
-            }) else {
-                return Task::ready(Ok(color));
-            };
-            cx.background_spawn(async move {
-                let resolve_task = lang_server.request::<lsp::request::ColorPresentationRequest>(
-                    lsp::ColorPresentationParams {
-                        text_document: make_text_document_identifier(&path)?,
-                        color: color.color,
-                        range: color.lsp_range,
-                        work_done_progress_params: Default::default(),
-                        partial_result_params: Default::default(),
-                    },
-                );
-                color.color_presentations = resolve_task
-                    .await
-                    .into_response()
-                    .context("color presentation resolve LSP request")?
-                    .into_iter()
-                    .map(|presentation| ColorPresentation {
-                        label: SharedString::from(presentation.label),
-                        text_edit: presentation.text_edit,
-                        additional_text_edits: presentation
-                            .additional_text_edits
-                            .unwrap_or_default(),
-                    })
-                    .collect();
-                color.resolved = true;
-                Ok(color)
-            })
-        }
-    }
-
     pub(crate) fn linked_edits(
         &mut self,
         buffer: &Entity<Buffer>,
@@ -6165,169 +5985,6 @@ impl LspStore {
         }
     }
 
-    pub fn code_lens_actions(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) -> CodeLensTask {
-        let version_queried_for = buffer.read(cx).version();
-        let buffer_id = buffer.read(cx).remote_id();
-        let existing_servers = self.as_local().map(|local| {
-            local
-                .buffers_opened_in_servers
-                .get(&buffer_id)
-                .cloned()
-                .unwrap_or_default()
-        });
-
-        if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
-            if let Some(cached_lens) = &lsp_data.code_lens {
-                if !version_queried_for.changed_since(&lsp_data.buffer_version) {
-                    let has_different_servers = existing_servers.is_some_and(|existing_servers| {
-                        existing_servers != cached_lens.lens.keys().copied().collect()
-                    });
-                    if !has_different_servers {
-                        return Task::ready(Ok(Some(
-                            cached_lens.lens.values().flatten().cloned().collect(),
-                        )))
-                        .shared();
-                    }
-                } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() {
-                    if !version_queried_for.changed_since(updating_for) {
-                        return running_update.clone();
-                    }
-                }
-            }
-        }
-
-        let lens_lsp_data = self
-            .latest_lsp_data(buffer, cx)
-            .code_lens
-            .get_or_insert_default();
-        let buffer = buffer.clone();
-        let query_version_queried_for = version_queried_for.clone();
-        let new_task = cx
-            .spawn(async move |lsp_store, cx| {
-                cx.background_executor()
-                    .timer(Duration::from_millis(30))
-                    .await;
-                let fetched_lens = lsp_store
-                    .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx))
-                    .map_err(Arc::new)?
-                    .await
-                    .context("fetching code lens")
-                    .map_err(Arc::new);
-                let fetched_lens = match fetched_lens {
-                    Ok(fetched_lens) => fetched_lens,
-                    Err(e) => {
-                        lsp_store
-                            .update(cx, |lsp_store, _| {
-                                if let Some(lens_lsp_data) = lsp_store
-                                    .lsp_data
-                                    .get_mut(&buffer_id)
-                                    .and_then(|lsp_data| lsp_data.code_lens.as_mut())
-                                {
-                                    lens_lsp_data.update = None;
-                                }
-                            })
-                            .ok();
-                        return Err(e);
-                    }
-                };
-
-                lsp_store
-                    .update(cx, |lsp_store, _| {
-                        let lsp_data = lsp_store.current_lsp_data(buffer_id)?;
-                        let code_lens = lsp_data.code_lens.as_mut()?;
-                        if let Some(fetched_lens) = fetched_lens {
-                            if lsp_data.buffer_version == query_version_queried_for {
-                                code_lens.lens.extend(fetched_lens);
-                            } else if !lsp_data
-                                .buffer_version
-                                .changed_since(&query_version_queried_for)
-                            {
-                                lsp_data.buffer_version = query_version_queried_for;
-                                code_lens.lens = fetched_lens;
-                            }
-                        }
-                        code_lens.update = None;
-                        Some(code_lens.lens.values().flatten().cloned().collect())
-                    })
-                    .map_err(Arc::new)
-            })
-            .shared();
-        lens_lsp_data.update = Some((version_queried_for, new_task.clone()));
-        new_task
-    }
-
-    fn fetch_code_lens(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Option<HashMap<LanguageServerId, Vec<CodeAction>>>>> {
-        if let Some((upstream_client, project_id)) = self.upstream_client() {
-            let request = GetCodeLens;
-            if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(None));
-            }
-            let request_task = upstream_client.request_lsp(
-                project_id,
-                None,
-                LSP_REQUEST_TIMEOUT,
-                cx.background_executor().clone(),
-                request.to_proto(project_id, buffer.read(cx)),
-            );
-            let buffer = buffer.clone();
-            cx.spawn(async move |weak_lsp_store, cx| {
-                let Some(lsp_store) = weak_lsp_store.upgrade() else {
-                    return Ok(None);
-                };
-                let Some(responses) = request_task.await? else {
-                    return Ok(None);
-                };
-
-                let code_lens_actions = join_all(responses.payload.into_iter().map(|response| {
-                    let lsp_store = lsp_store.clone();
-                    let buffer = buffer.clone();
-                    let cx = cx.clone();
-                    async move {
-                        (
-                            LanguageServerId::from_proto(response.server_id),
-                            GetCodeLens
-                                .response_from_proto(response.response, lsp_store, buffer, cx)
-                                .await,
-                        )
-                    }
-                }))
-                .await;
-
-                let mut has_errors = false;
-                let code_lens_actions = code_lens_actions
-                    .into_iter()
-                    .filter_map(|(server_id, code_lens)| match code_lens {
-                        Ok(code_lens) => Some((server_id, code_lens)),
-                        Err(e) => {
-                            has_errors = true;
-                            log::error!("{e:#}");
-                            None
-                        }
-                    })
-                    .collect::<HashMap<_, _>>();
-                anyhow::ensure!(
-                    !has_errors || !code_lens_actions.is_empty(),
-                    "Failed to fetch code lens"
-                );
-                Ok(Some(code_lens_actions))
-            })
-        } else {
-            let code_lens_actions_task =
-                self.request_multiple_lsp_locally(buffer, None::<usize>, GetCodeLens, cx);
-            cx.background_spawn(async move {
-                Ok(Some(code_lens_actions_task.await.into_iter().collect()))
-            })
-        }
-    }
-
     #[inline(never)]
     pub fn completions(
         &self,
@@ -7564,213 +7221,6 @@ impl LspStore {
         })
     }
 
-    pub fn document_colors(
-        &mut self,
-        known_cache_version: Option<usize>,
-        buffer: Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) -> Option<DocumentColorTask> {
-        let version_queried_for = buffer.read(cx).version();
-        let buffer_id = buffer.read(cx).remote_id();
-
-        let current_language_servers = self.as_local().map(|local| {
-            local
-                .buffers_opened_in_servers
-                .get(&buffer_id)
-                .cloned()
-                .unwrap_or_default()
-        });
-
-        if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
-            if let Some(cached_colors) = &lsp_data.document_colors {
-                if !version_queried_for.changed_since(&lsp_data.buffer_version) {
-                    let has_different_servers =
-                        current_language_servers.is_some_and(|current_language_servers| {
-                            current_language_servers
-                                != cached_colors.colors.keys().copied().collect()
-                        });
-                    if !has_different_servers {
-                        let cache_version = cached_colors.cache_version;
-                        if Some(cache_version) == known_cache_version {
-                            return None;
-                        } else {
-                            return Some(
-                                Task::ready(Ok(DocumentColors {
-                                    colors: cached_colors
-                                        .colors
-                                        .values()
-                                        .flatten()
-                                        .cloned()
-                                        .collect(),
-                                    cache_version: Some(cache_version),
-                                }))
-                                .shared(),
-                            );
-                        }
-                    }
-                }
-            }
-        }
-
-        let color_lsp_data = self
-            .latest_lsp_data(&buffer, cx)
-            .document_colors
-            .get_or_insert_default();
-        if let Some((updating_for, running_update)) = &color_lsp_data.colors_update
-            && !version_queried_for.changed_since(updating_for)
-        {
-            return Some(running_update.clone());
-        }
-        let buffer_version_queried_for = version_queried_for.clone();
-        let new_task = cx
-            .spawn(async move |lsp_store, cx| {
-                cx.background_executor()
-                    .timer(Duration::from_millis(30))
-                    .await;
-                let fetched_colors = lsp_store
-                    .update(cx, |lsp_store, cx| {
-                        lsp_store.fetch_document_colors_for_buffer(&buffer, cx)
-                    })?
-                    .await
-                    .context("fetching document colors")
-                    .map_err(Arc::new);
-                let fetched_colors = match fetched_colors {
-                    Ok(fetched_colors) => {
-                        if buffer.update(cx, |buffer, _| {
-                            buffer.version() != buffer_version_queried_for
-                        }) {
-                            return Ok(DocumentColors::default());
-                        }
-                        fetched_colors
-                    }
-                    Err(e) => {
-                        lsp_store
-                            .update(cx, |lsp_store, _| {
-                                if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) {
-                                    if let Some(document_colors) = &mut lsp_data.document_colors {
-                                        document_colors.colors_update = None;
-                                    }
-                                }
-                            })
-                            .ok();
-                        return Err(e);
-                    }
-                };
-
-                lsp_store
-                    .update(cx, |lsp_store, cx| {
-                        let lsp_data = lsp_store.latest_lsp_data(&buffer, cx);
-                        let lsp_colors = lsp_data.document_colors.get_or_insert_default();
-
-                        if let Some(fetched_colors) = fetched_colors {
-                            if lsp_data.buffer_version == buffer_version_queried_for {
-                                lsp_colors.colors.extend(fetched_colors);
-                                lsp_colors.cache_version += 1;
-                            } else if !lsp_data
-                                .buffer_version
-                                .changed_since(&buffer_version_queried_for)
-                            {
-                                lsp_data.buffer_version = buffer_version_queried_for;
-                                lsp_colors.colors = fetched_colors;
-                                lsp_colors.cache_version += 1;
-                            }
-                        }
-                        lsp_colors.colors_update = None;
-                        let colors = lsp_colors
-                            .colors
-                            .values()
-                            .flatten()
-                            .cloned()
-                            .collect::<HashSet<_>>();
-                        DocumentColors {
-                            colors,
-                            cache_version: Some(lsp_colors.cache_version),
-                        }
-                    })
-                    .map_err(Arc::new)
-            })
-            .shared();
-        color_lsp_data.colors_update = Some((version_queried_for, new_task.clone()));
-        Some(new_task)
-    }
-
-    fn fetch_document_colors_for_buffer(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<Option<HashMap<LanguageServerId, HashSet<DocumentColor>>>>> {
-        if let Some((client, project_id)) = self.upstream_client() {
-            let request = GetDocumentColor {};
-            if !self.is_capable_for_proto_request(buffer, &request, cx) {
-                return Task::ready(Ok(None));
-            }
-
-            let request_task = client.request_lsp(
-                project_id,
-                None,
-                LSP_REQUEST_TIMEOUT,
-                cx.background_executor().clone(),
-                request.to_proto(project_id, buffer.read(cx)),
-            );
-            let buffer = buffer.clone();
-            cx.spawn(async move |lsp_store, cx| {
-                let Some(lsp_store) = lsp_store.upgrade() else {
-                    return Ok(None);
-                };
-                let colors = join_all(
-                    request_task
-                        .await
-                        .log_err()
-                        .flatten()
-                        .map(|response| response.payload)
-                        .unwrap_or_default()
-                        .into_iter()
-                        .map(|color_response| {
-                            let response = request.response_from_proto(
-                                color_response.response,
-                                lsp_store.clone(),
-                                buffer.clone(),
-                                cx.clone(),
-                            );
-                            async move {
-                                (
-                                    LanguageServerId::from_proto(color_response.server_id),
-                                    response.await.log_err().unwrap_or_default(),
-                                )
-                            }
-                        }),
-                )
-                .await
-                .into_iter()
-                .fold(HashMap::default(), |mut acc, (server_id, colors)| {
-                    acc.entry(server_id)
-                        .or_insert_with(HashSet::default)
-                        .extend(colors);
-                    acc
-                });
-                Ok(Some(colors))
-            })
-        } else {
-            let document_colors_task =
-                self.request_multiple_lsp_locally(buffer, None::<usize>, GetDocumentColor, cx);
-            cx.background_spawn(async move {
-                Ok(Some(
-                    document_colors_task
-                        .await
-                        .into_iter()
-                        .fold(HashMap::default(), |mut acc, (server_id, colors)| {
-                            acc.entry(server_id)
-                                .or_insert_with(HashSet::default)
-                                .extend(colors);
-                            acc
-                        })
-                        .into_iter()
-                        .collect(),
-                ))
-            })
-        }
-    }
-
     pub fn signature_help<T: ToPointUtf16>(
         &mut self,
         buffer: &Entity<Buffer>,
@@ -10544,20 +9994,6 @@ impl LspStore {
         Ok(proto::OnTypeFormattingResponse { transaction })
     }
 
-    async fn handle_refresh_inlay_hints(
-        lsp_store: Entity<Self>,
-        envelope: TypedEnvelope<proto::RefreshInlayHints>,
-        mut cx: AsyncApp,
-    ) -> Result<proto::Ack> {
-        lsp_store.update(&mut cx, |_, cx| {
-            cx.emit(LspStoreEvent::RefreshInlayHints {
-                server_id: LanguageServerId::from_proto(envelope.payload.server_id),
-                request_id: envelope.payload.request_id.map(|id| id as usize),
-            });
-        });
-        Ok(proto::Ack {})
-    }
-
     async fn handle_pull_workspace_diagnostics(
         lsp_store: Entity<Self>,
         envelope: TypedEnvelope<proto::PullWorkspaceDiagnostics>,
@@ -10570,112 +10006,6 @@ impl LspStore {
         Ok(proto::Ack {})
     }
 
-    async fn handle_get_color_presentation(
-        lsp_store: Entity<Self>,
-        envelope: TypedEnvelope<proto::GetColorPresentation>,
-        mut cx: AsyncApp,
-    ) -> Result<proto::GetColorPresentationResponse> {
-        let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
-        let buffer = lsp_store.update(&mut cx, |lsp_store, cx| {
-            lsp_store.buffer_store.read(cx).get_existing(buffer_id)
-        })?;
-
-        let color = envelope
-            .payload
-            .color
-            .context("invalid color resolve request")?;
-        let start = color
-            .lsp_range_start
-            .context("invalid color resolve request")?;
-        let end = color
-            .lsp_range_end
-            .context("invalid color resolve request")?;
-
-        let color = DocumentColor {
-            lsp_range: lsp::Range {
-                start: point_to_lsp(PointUtf16::new(start.row, start.column)),
-                end: point_to_lsp(PointUtf16::new(end.row, end.column)),
-            },
-            color: lsp::Color {
-                red: color.red,
-                green: color.green,
-                blue: color.blue,
-                alpha: color.alpha,
-            },
-            resolved: false,
-            color_presentations: Vec::new(),
-        };
-        let resolved_color = lsp_store
-            .update(&mut cx, |lsp_store, cx| {
-                lsp_store.resolve_color_presentation(
-                    color,
-                    buffer.clone(),
-                    LanguageServerId(envelope.payload.server_id as usize),
-                    cx,
-                )
-            })
-            .await
-            .context("resolving color presentation")?;
-
-        Ok(proto::GetColorPresentationResponse {
-            presentations: resolved_color
-                .color_presentations
-                .into_iter()
-                .map(|presentation| proto::ColorPresentation {
-                    label: presentation.label.to_string(),
-                    text_edit: presentation.text_edit.map(serialize_lsp_edit),
-                    additional_text_edits: presentation
-                        .additional_text_edits
-                        .into_iter()
-                        .map(serialize_lsp_edit)
-                        .collect(),
-                })
-                .collect(),
-        })
-    }
-
-    async fn handle_resolve_inlay_hint(
-        lsp_store: Entity<Self>,
-        envelope: TypedEnvelope<proto::ResolveInlayHint>,
-        mut cx: AsyncApp,
-    ) -> Result<proto::ResolveInlayHintResponse> {
-        let proto_hint = envelope
-            .payload
-            .hint
-            .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint");
-        let hint = InlayHints::proto_to_project_hint(proto_hint)
-            .context("resolved proto inlay hint conversion")?;
-        let buffer = lsp_store.update(&mut cx, |lsp_store, cx| {
-            let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
-            lsp_store.buffer_store.read(cx).get_existing(buffer_id)
-        })?;
-        let response_hint = lsp_store
-            .update(&mut cx, |lsp_store, cx| {
-                lsp_store.resolve_inlay_hint(
-                    hint,
-                    buffer,
-                    LanguageServerId(envelope.payload.language_server_id as usize),
-                    cx,
-                )
-            })
-            .await
-            .context("inlay hints fetch")?;
-        Ok(proto::ResolveInlayHintResponse {
-            hint: Some(InlayHints::project_to_proto_hint(response_hint)),
-        })
-    }
-
-    async fn handle_refresh_code_lens(
-        this: Entity<Self>,
-        _: TypedEnvelope<proto::RefreshCodeLens>,
-        mut cx: AsyncApp,
-    ) -> Result<proto::Ack> {
-        this.update(&mut cx, |_, cx| {
-            cx.emit(LspStoreEvent::RefreshCodeLens);
-        });
-        Ok(proto::Ack {})
-    }
-
     async fn handle_open_buffer_for_symbol(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::OpenBufferForSymbol>,
@@ -13490,19 +12820,6 @@ impl LspStore {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option<CodeLensTask> {
-        Some(
-            self.lsp_data
-                .get_mut(&buffer_id)?
-                .code_lens
-                .take()?
-                .update
-                .take()?
-                .1,
-        )
-    }
-
     pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> {
         self.downstream_client.clone()
     }

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

@@ -0,0 +1,223 @@
+use std::sync::Arc;
+
+use anyhow::{Context as _, Result};
+use clock::Global;
+use collections::HashMap;
+use futures::{
+    FutureExt as _,
+    future::{Shared, join_all},
+};
+use gpui::{AppContext as _, AsyncApp, Context, Entity, Task};
+use language::Buffer;
+use lsp::{LSP_REQUEST_TIMEOUT, LanguageServerId};
+use rpc::{TypedEnvelope, proto};
+use std::time::Duration;
+
+use crate::{
+    CodeAction, LspStore, LspStoreEvent,
+    lsp_command::{GetCodeLens, LspCommand as _},
+};
+
+pub(super) type CodeLensTask =
+    Shared<Task<std::result::Result<Option<Vec<CodeAction>>, Arc<anyhow::Error>>>>;
+
+#[derive(Debug, Default)]
+pub(super) struct CodeLensData {
+    pub(super) lens: HashMap<LanguageServerId, Vec<CodeAction>>,
+    pub(super) update: Option<(Global, CodeLensTask)>,
+}
+
+impl CodeLensData {
+    pub(super) fn remove_server_data(&mut self, server_id: LanguageServerId) {
+        self.lens.remove(&server_id);
+    }
+}
+
+impl LspStore {
+    pub fn code_lens_actions(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> CodeLensTask {
+        let version_queried_for = buffer.read(cx).version();
+        let buffer_id = buffer.read(cx).remote_id();
+        let existing_servers = self.as_local().map(|local| {
+            local
+                .buffers_opened_in_servers
+                .get(&buffer_id)
+                .cloned()
+                .unwrap_or_default()
+        });
+
+        if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
+            if let Some(cached_lens) = &lsp_data.code_lens {
+                if !version_queried_for.changed_since(&lsp_data.buffer_version) {
+                    let has_different_servers = existing_servers.is_some_and(|existing_servers| {
+                        existing_servers != cached_lens.lens.keys().copied().collect()
+                    });
+                    if !has_different_servers {
+                        return Task::ready(Ok(Some(
+                            cached_lens.lens.values().flatten().cloned().collect(),
+                        )))
+                        .shared();
+                    }
+                } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() {
+                    if !version_queried_for.changed_since(updating_for) {
+                        return running_update.clone();
+                    }
+                }
+            }
+        }
+
+        let lens_lsp_data = self
+            .latest_lsp_data(buffer, cx)
+            .code_lens
+            .get_or_insert_default();
+        let buffer = buffer.clone();
+        let query_version_queried_for = version_queried_for.clone();
+        let new_task = cx
+            .spawn(async move |lsp_store, cx| {
+                cx.background_executor()
+                    .timer(Duration::from_millis(30))
+                    .await;
+                let fetched_lens = lsp_store
+                    .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx))
+                    .map_err(Arc::new)?
+                    .await
+                    .context("fetching code lens")
+                    .map_err(Arc::new);
+                let fetched_lens = match fetched_lens {
+                    Ok(fetched_lens) => fetched_lens,
+                    Err(e) => {
+                        lsp_store
+                            .update(cx, |lsp_store, _| {
+                                if let Some(lens_lsp_data) = lsp_store
+                                    .lsp_data
+                                    .get_mut(&buffer_id)
+                                    .and_then(|lsp_data| lsp_data.code_lens.as_mut())
+                                {
+                                    lens_lsp_data.update = None;
+                                }
+                            })
+                            .ok();
+                        return Err(e);
+                    }
+                };
+
+                lsp_store
+                    .update(cx, |lsp_store, _| {
+                        let lsp_data = lsp_store.current_lsp_data(buffer_id)?;
+                        let code_lens = lsp_data.code_lens.as_mut()?;
+                        if let Some(fetched_lens) = fetched_lens {
+                            if lsp_data.buffer_version == query_version_queried_for {
+                                code_lens.lens.extend(fetched_lens);
+                            } else if !lsp_data
+                                .buffer_version
+                                .changed_since(&query_version_queried_for)
+                            {
+                                lsp_data.buffer_version = query_version_queried_for;
+                                code_lens.lens = fetched_lens;
+                            }
+                        }
+                        code_lens.update = None;
+                        Some(code_lens.lens.values().flatten().cloned().collect())
+                    })
+                    .map_err(Arc::new)
+            })
+            .shared();
+        lens_lsp_data.update = Some((version_queried_for, new_task.clone()));
+        new_task
+    }
+
+    pub(super) fn fetch_code_lens(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Option<HashMap<LanguageServerId, Vec<CodeAction>>>>> {
+        if let Some((upstream_client, project_id)) = self.upstream_client() {
+            let request = GetCodeLens;
+            if !self.is_capable_for_proto_request(buffer, &request, cx) {
+                return Task::ready(Ok(None));
+            }
+            let request_task = upstream_client.request_lsp(
+                project_id,
+                None,
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
+            let buffer = buffer.clone();
+            cx.spawn(async move |weak_lsp_store, cx| {
+                let Some(lsp_store) = weak_lsp_store.upgrade() else {
+                    return Ok(None);
+                };
+                let Some(responses) = request_task.await? else {
+                    return Ok(None);
+                };
+
+                let code_lens_actions = join_all(responses.payload.into_iter().map(|response| {
+                    let lsp_store = lsp_store.clone();
+                    let buffer = buffer.clone();
+                    let cx = cx.clone();
+                    async move {
+                        (
+                            LanguageServerId::from_proto(response.server_id),
+                            GetCodeLens
+                                .response_from_proto(response.response, lsp_store, buffer, cx)
+                                .await,
+                        )
+                    }
+                }))
+                .await;
+
+                let mut has_errors = false;
+                let code_lens_actions = code_lens_actions
+                    .into_iter()
+                    .filter_map(|(server_id, code_lens)| match code_lens {
+                        Ok(code_lens) => Some((server_id, code_lens)),
+                        Err(e) => {
+                            has_errors = true;
+                            log::error!("{e:#}");
+                            None
+                        }
+                    })
+                    .collect::<HashMap<_, _>>();
+                anyhow::ensure!(
+                    !has_errors || !code_lens_actions.is_empty(),
+                    "Failed to fetch code lens"
+                );
+                Ok(Some(code_lens_actions))
+            })
+        } else {
+            let code_lens_actions_task =
+                self.request_multiple_lsp_locally(buffer, None::<usize>, GetCodeLens, cx);
+            cx.background_spawn(async move {
+                Ok(Some(code_lens_actions_task.await.into_iter().collect()))
+            })
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn forget_code_lens_task(&mut self, buffer_id: text::BufferId) -> Option<CodeLensTask> {
+        Some(
+            self.lsp_data
+                .get_mut(&buffer_id)?
+                .code_lens
+                .take()?
+                .update
+                .take()?
+                .1,
+        )
+    }
+
+    pub(super) async fn handle_refresh_code_lens(
+        this: Entity<Self>,
+        _: TypedEnvelope<proto::RefreshCodeLens>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        this.update(&mut cx, |_, cx| {
+            cx.emit(LspStoreEvent::RefreshCodeLens);
+        });
+        Ok(proto::Ack {})
+    }
+}

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

@@ -0,0 +1,401 @@
+use std::{sync::Arc, time::Duration};
+
+use anyhow::{Context as _, Result};
+use clock::Global;
+use collections::{HashMap, HashSet};
+use futures::{
+    FutureExt as _,
+    future::{Shared, join_all},
+};
+use gpui::{AppContext as _, AsyncApp, Context, Entity, SharedString, Task};
+use language::{
+    Buffer, LocalFile as _, PointUtf16, point_to_lsp,
+    proto::{deserialize_lsp_edit, serialize_lsp_edit},
+};
+use lsp::{LSP_REQUEST_TIMEOUT, LanguageServerId};
+use rpc::{TypedEnvelope, proto};
+use text::BufferId;
+use util::ResultExt as _;
+use worktree::File;
+
+use crate::{
+    ColorPresentation, DocumentColor, LspStore,
+    lsp_command::{GetDocumentColor, LspCommand as _, make_text_document_identifier},
+};
+
+#[derive(Debug, Default, Clone)]
+pub struct DocumentColors {
+    pub colors: HashSet<DocumentColor>,
+}
+
+pub(super) type DocumentColorTask =
+    Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>;
+
+#[derive(Debug, Default)]
+pub(super) struct DocumentColorData {
+    pub(super) colors: HashMap<LanguageServerId, HashSet<DocumentColor>>,
+    pub(super) colors_update: Option<(Global, DocumentColorTask)>,
+}
+
+impl DocumentColorData {
+    pub(super) fn remove_server_data(&mut self, server_id: LanguageServerId) {
+        self.colors.remove(&server_id);
+    }
+}
+
+impl LspStore {
+    pub fn document_colors(
+        &mut self,
+        buffer: Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Option<DocumentColorTask> {
+        let version_queried_for = buffer.read(cx).version();
+        let buffer_id = buffer.read(cx).remote_id();
+
+        let current_language_servers = self.as_local().map(|local| {
+            local
+                .buffers_opened_in_servers
+                .get(&buffer_id)
+                .cloned()
+                .unwrap_or_default()
+        });
+
+        if let Some(lsp_data) = self.current_lsp_data(buffer_id) {
+            if let Some(cached_colors) = &lsp_data.document_colors {
+                if !version_queried_for.changed_since(&lsp_data.buffer_version) {
+                    let has_different_servers =
+                        current_language_servers.is_some_and(|current_language_servers| {
+                            current_language_servers
+                                != cached_colors.colors.keys().copied().collect()
+                        });
+                    if !has_different_servers {
+                        return Some(
+                            Task::ready(Ok(DocumentColors {
+                                colors: cached_colors.colors.values().flatten().cloned().collect(),
+                            }))
+                            .shared(),
+                        );
+                    }
+                }
+            }
+        }
+
+        let color_lsp_data = self
+            .latest_lsp_data(&buffer, cx)
+            .document_colors
+            .get_or_insert_default();
+        if let Some((updating_for, running_update)) = &color_lsp_data.colors_update
+            && !version_queried_for.changed_since(updating_for)
+        {
+            return Some(running_update.clone());
+        }
+        let buffer_version_queried_for = version_queried_for.clone();
+        let new_task = cx
+            .spawn(async move |lsp_store, cx| {
+                cx.background_executor()
+                    .timer(Duration::from_millis(30))
+                    .await;
+                let fetched_colors = lsp_store
+                    .update(cx, |lsp_store, cx| {
+                        lsp_store.fetch_document_colors_for_buffer(&buffer, cx)
+                    })?
+                    .await
+                    .context("fetching document colors")
+                    .map_err(Arc::new);
+                let fetched_colors = match fetched_colors {
+                    Ok(fetched_colors) => {
+                        if buffer.update(cx, |buffer, _| {
+                            buffer.version() != buffer_version_queried_for
+                        }) {
+                            return Ok(DocumentColors::default());
+                        }
+                        fetched_colors
+                    }
+                    Err(e) => {
+                        lsp_store
+                            .update(cx, |lsp_store, _| {
+                                if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) {
+                                    if let Some(document_colors) = &mut lsp_data.document_colors {
+                                        document_colors.colors_update = None;
+                                    }
+                                }
+                            })
+                            .ok();
+                        return Err(e);
+                    }
+                };
+
+                lsp_store
+                    .update(cx, |lsp_store, cx| {
+                        let lsp_data = lsp_store.latest_lsp_data(&buffer, cx);
+                        let lsp_colors = lsp_data.document_colors.get_or_insert_default();
+
+                        if let Some(fetched_colors) = fetched_colors {
+                            if lsp_data.buffer_version == buffer_version_queried_for {
+                                lsp_colors.colors.extend(fetched_colors);
+                            } else if !lsp_data
+                                .buffer_version
+                                .changed_since(&buffer_version_queried_for)
+                            {
+                                lsp_data.buffer_version = buffer_version_queried_for;
+                                lsp_colors.colors = fetched_colors;
+                            }
+                        }
+                        lsp_colors.colors_update = None;
+                        let colors = lsp_colors
+                            .colors
+                            .values()
+                            .flatten()
+                            .cloned()
+                            .collect::<HashSet<_>>();
+                        DocumentColors { colors }
+                    })
+                    .map_err(Arc::new)
+            })
+            .shared();
+        color_lsp_data.colors_update = Some((version_queried_for, new_task.clone()));
+        Some(new_task)
+    }
+
+    pub fn resolve_color_presentation(
+        &mut self,
+        mut color: DocumentColor,
+        buffer: Entity<Buffer>,
+        server_id: LanguageServerId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<DocumentColor>> {
+        if color.resolved {
+            return Task::ready(Ok(color));
+        }
+
+        if let Some((upstream_client, project_id)) = self.upstream_client() {
+            let start = color.lsp_range.start;
+            let end = color.lsp_range.end;
+            let request = proto::GetColorPresentation {
+                project_id,
+                server_id: server_id.to_proto(),
+                buffer_id: buffer.read(cx).remote_id().into(),
+                color: Some(proto::ColorInformation {
+                    red: color.color.red,
+                    green: color.color.green,
+                    blue: color.color.blue,
+                    alpha: color.color.alpha,
+                    lsp_range_start: Some(proto::PointUtf16 {
+                        row: start.line,
+                        column: start.character,
+                    }),
+                    lsp_range_end: Some(proto::PointUtf16 {
+                        row: end.line,
+                        column: end.character,
+                    }),
+                }),
+            };
+            cx.background_spawn(async move {
+                let response = upstream_client
+                    .request(request)
+                    .await
+                    .context("color presentation proto request")?;
+                color.resolved = true;
+                color.color_presentations = response
+                    .presentations
+                    .into_iter()
+                    .map(|presentation| ColorPresentation {
+                        label: SharedString::from(presentation.label),
+                        text_edit: presentation.text_edit.and_then(deserialize_lsp_edit),
+                        additional_text_edits: presentation
+                            .additional_text_edits
+                            .into_iter()
+                            .filter_map(deserialize_lsp_edit)
+                            .collect(),
+                    })
+                    .collect();
+                Ok(color)
+            })
+        } else {
+            let path = match buffer
+                .update(cx, |buffer, cx| {
+                    Some(File::from_dyn(buffer.file())?.abs_path(cx))
+                })
+                .context("buffer with the missing path")
+            {
+                Ok(path) => path,
+                Err(e) => return Task::ready(Err(e)),
+            };
+            let Some(lang_server) = buffer.update(cx, |buffer, cx| {
+                self.language_server_for_local_buffer(buffer, server_id, cx)
+                    .map(|(_, server)| server.clone())
+            }) else {
+                return Task::ready(Ok(color));
+            };
+            cx.background_spawn(async move {
+                let resolve_task = lang_server.request::<lsp::request::ColorPresentationRequest>(
+                    lsp::ColorPresentationParams {
+                        text_document: make_text_document_identifier(&path)?,
+                        color: color.color,
+                        range: color.lsp_range,
+                        work_done_progress_params: Default::default(),
+                        partial_result_params: Default::default(),
+                    },
+                );
+                color.color_presentations = resolve_task
+                    .await
+                    .into_response()
+                    .context("color presentation resolve LSP request")?
+                    .into_iter()
+                    .map(|presentation| ColorPresentation {
+                        label: SharedString::from(presentation.label),
+                        text_edit: presentation.text_edit,
+                        additional_text_edits: presentation
+                            .additional_text_edits
+                            .unwrap_or_default(),
+                    })
+                    .collect();
+                color.resolved = true;
+                Ok(color)
+            })
+        }
+    }
+
+    pub(super) fn fetch_document_colors_for_buffer(
+        &mut self,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<Option<HashMap<LanguageServerId, HashSet<DocumentColor>>>>> {
+        if let Some((client, project_id)) = self.upstream_client() {
+            let request = GetDocumentColor {};
+            if !self.is_capable_for_proto_request(buffer, &request, cx) {
+                return Task::ready(Ok(None));
+            }
+
+            let request_task = client.request_lsp(
+                project_id,
+                None,
+                LSP_REQUEST_TIMEOUT,
+                cx.background_executor().clone(),
+                request.to_proto(project_id, buffer.read(cx)),
+            );
+            let buffer = buffer.clone();
+            cx.spawn(async move |lsp_store, cx| {
+                let Some(lsp_store) = lsp_store.upgrade() else {
+                    return Ok(None);
+                };
+                let colors: HashMap<LanguageServerId, HashSet<DocumentColor>> = join_all(
+                    request_task
+                        .await
+                        .log_err()
+                        .flatten()
+                        .map(|response| response.payload)
+                        .unwrap_or_default()
+                        .into_iter()
+                        .map(|color_response| {
+                            let response = request.response_from_proto(
+                                color_response.response,
+                                lsp_store.clone(),
+                                buffer.clone(),
+                                cx.clone(),
+                            );
+                            async move {
+                                let colors: Vec<DocumentColor> =
+                                    response.await.log_err().unwrap_or_default();
+                                (
+                                    LanguageServerId::from_proto(color_response.server_id),
+                                    colors,
+                                )
+                            }
+                        }),
+                )
+                .await
+                .into_iter()
+                .fold(HashMap::default(), |mut acc, (server_id, colors)| {
+                    acc.entry(server_id)
+                        .or_insert_with(HashSet::default)
+                        .extend(colors);
+                    acc
+                });
+                Ok(Some(colors))
+            })
+        } else {
+            let document_colors_task =
+                self.request_multiple_lsp_locally(buffer, None::<usize>, GetDocumentColor, cx);
+            cx.background_spawn(async move {
+                Ok(Some(
+                    document_colors_task
+                        .await
+                        .into_iter()
+                        .fold(HashMap::default(), |mut acc, (server_id, colors)| {
+                            acc.entry(server_id)
+                                .or_insert_with(HashSet::default)
+                                .extend(colors);
+                            acc
+                        })
+                        .into_iter()
+                        .collect(),
+                ))
+            })
+        }
+    }
+
+    pub(super) async fn handle_get_color_presentation(
+        lsp_store: Entity<Self>,
+        envelope: TypedEnvelope<proto::GetColorPresentation>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::GetColorPresentationResponse> {
+        let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
+        let buffer = lsp_store.update(&mut cx, |lsp_store, cx| {
+            lsp_store.buffer_store.read(cx).get_existing(buffer_id)
+        })?;
+
+        let color = envelope
+            .payload
+            .color
+            .context("invalid color resolve request")?;
+        let start = color
+            .lsp_range_start
+            .context("invalid color resolve request")?;
+        let end = color
+            .lsp_range_end
+            .context("invalid color resolve request")?;
+
+        let color = DocumentColor {
+            lsp_range: lsp::Range {
+                start: point_to_lsp(PointUtf16::new(start.row, start.column)),
+                end: point_to_lsp(PointUtf16::new(end.row, end.column)),
+            },
+            color: lsp::Color {
+                red: color.red,
+                green: color.green,
+                blue: color.blue,
+                alpha: color.alpha,
+            },
+            resolved: false,
+            color_presentations: Vec::new(),
+        };
+        let resolved_color = lsp_store
+            .update(&mut cx, |lsp_store, cx| {
+                lsp_store.resolve_color_presentation(
+                    color,
+                    buffer.clone(),
+                    LanguageServerId(envelope.payload.server_id as usize),
+                    cx,
+                )
+            })
+            .await
+            .context("resolving color presentation")?;
+
+        Ok(proto::GetColorPresentationResponse {
+            presentations: resolved_color
+                .color_presentations
+                .into_iter()
+                .map(|presentation| proto::ColorPresentation {
+                    label: presentation.label.to_string(),
+                    text_edit: presentation.text_edit.map(serialize_lsp_edit),
+                    additional_text_edits: presentation
+                        .additional_text_edits
+                        .into_iter()
+                        .map(serialize_lsp_edit)
+                        .collect(),
+                })
+                .collect(),
+        })
+    }
+}

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

@@ -1,16 +1,18 @@
 use std::{collections::hash_map, ops::Range, sync::Arc};
 
+use anyhow::{Context as _, Result};
 use collections::HashMap;
 use futures::future::Shared;
-use gpui::{App, Entity, Task};
+use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task};
 use language::{
     Buffer,
     row_chunk::{RowChunk, RowChunks},
 };
 use lsp::LanguageServerId;
-use text::Point;
+use rpc::{TypedEnvelope, proto};
+use text::{BufferId, Point};
 
-use crate::{InlayHint, InlayId};
+use crate::{InlayHint, InlayId, LspStore, LspStoreEvent, ResolveState, lsp_command::InlayHints};
 
 pub type CacheInlayHints = HashMap<LanguageServerId, Vec<(InlayId, InlayHint)>>;
 pub type CacheInlayHintsTask = Shared<Task<Result<CacheInlayHints, Arc<anyhow::Error>>>>;
@@ -224,3 +226,113 @@ impl BufferInlayHints {
         }
     }
 }
+
+impl LspStore {
+    pub(super) fn resolve_inlay_hint(
+        &self,
+        mut hint: InlayHint,
+        buffer: Entity<Buffer>,
+        server_id: LanguageServerId,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<InlayHint>> {
+        if let Some((upstream_client, project_id)) = self.upstream_client() {
+            if !self.check_if_capable_for_proto_request(&buffer, InlayHints::can_resolve_inlays, cx)
+            {
+                hint.resolve_state = ResolveState::Resolved;
+                return Task::ready(Ok(hint));
+            }
+            let request = proto::ResolveInlayHint {
+                project_id,
+                buffer_id: buffer.read(cx).remote_id().into(),
+                language_server_id: server_id.0 as u64,
+                hint: Some(InlayHints::project_to_proto_hint(hint.clone())),
+            };
+            cx.background_spawn(async move {
+                let response = upstream_client
+                    .request(request)
+                    .await
+                    .context("inlay hints proto request")?;
+                match response.hint {
+                    Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint)
+                        .context("inlay hints proto resolve response conversion"),
+                    None => Ok(hint),
+                }
+            })
+        } else {
+            let Some(lang_server) = buffer.update(cx, |buffer, cx| {
+                self.language_server_for_local_buffer(buffer, server_id, cx)
+                    .map(|(_, server)| server.clone())
+            }) else {
+                return Task::ready(Ok(hint));
+            };
+            if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) {
+                return Task::ready(Ok(hint));
+            }
+            let buffer_snapshot = buffer.read(cx).snapshot();
+            cx.spawn(async move |_, cx| {
+                let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
+                    InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
+                );
+                let resolved_hint = resolve_task
+                    .await
+                    .into_response()
+                    .context("inlay hint resolve LSP request")?;
+                let resolved_hint = InlayHints::lsp_to_project_hint(
+                    resolved_hint,
+                    &buffer,
+                    server_id,
+                    ResolveState::Resolved,
+                    false,
+                    cx,
+                )
+                .await?;
+                Ok(resolved_hint)
+            })
+        }
+    }
+
+    pub(super) async fn handle_refresh_inlay_hints(
+        lsp_store: Entity<Self>,
+        envelope: TypedEnvelope<proto::RefreshInlayHints>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        lsp_store.update(&mut cx, |_, cx| {
+            cx.emit(LspStoreEvent::RefreshInlayHints {
+                server_id: LanguageServerId::from_proto(envelope.payload.server_id),
+                request_id: envelope.payload.request_id.map(|id| id as usize),
+            });
+        });
+        Ok(proto::Ack {})
+    }
+
+    pub(super) async fn handle_resolve_inlay_hint(
+        lsp_store: Entity<Self>,
+        envelope: TypedEnvelope<proto::ResolveInlayHint>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::ResolveInlayHintResponse> {
+        let proto_hint = envelope
+            .payload
+            .hint
+            .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint");
+        let hint = InlayHints::proto_to_project_hint(proto_hint)
+            .context("resolved proto inlay hint conversion")?;
+        let buffer = lsp_store.update(&mut cx, |lsp_store, cx| {
+            let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
+            lsp_store.buffer_store.read(cx).get_existing(buffer_id)
+        })?;
+        let response_hint = lsp_store
+            .update(&mut cx, |lsp_store, cx| {
+                lsp_store.resolve_inlay_hint(
+                    hint,
+                    buffer,
+                    LanguageServerId(envelope.payload.language_server_id as usize),
+                    cx,
+                )
+            })
+            .await
+            .context("inlay hints fetch")?;
+        Ok(proto::ResolveInlayHintResponse {
+            hint: Some(InlayHints::project_to_proto_hint(response_hint)),
+        })
+    }
+}

crates/settings_ui/src/page_data.rs 🔗

@@ -8613,7 +8613,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> {
                     },
                 }),
                 metadata: None,
-                files: USER,
+                files: USER | PROJECT,
             }),
         ]
     }