Show inline previews for LSP document colors (#32816)

Kirill Bulatov created

https://github.com/user-attachments/assets/ad0fa304-e4fb-4598-877d-c02141f35d6f

Closes https://github.com/zed-industries/zed/issues/4678

Also adds the code to support `textDocument/colorPresentation`
counterpart that serves as a resolve mechanism for the document colors.
The resolve itself is not run though, and the editor does not
accommodate color presentations in the editor yet — until a well
described use case is provided.

Use `lsp_document_colors` editor settings to alter the presentation and
turn the feature off.

Release Notes:

- Start showing inline previews for LSP document colors

Change summary

assets/settings/default.json                 |  13 
crates/collab/src/rpc.rs                     |   1 
crates/collab/src/tests/editor_tests.rs      | 291 +++++++++
crates/diagnostics/src/diagnostics_tests.rs  |  12 
crates/editor/src/display_map.rs             |  10 
crates/editor/src/display_map/inlay_map.rs   | 127 ++-
crates/editor/src/editor.rs                  | 179 ++---
crates/editor/src/editor_settings.rs         |  21 
crates/editor/src/editor_tests.rs            |   4 
crates/editor/src/element.rs                 |  76 ++
crates/editor/src/lsp_colors.rs              | 358 ++++++++++++
crates/editor/src/movement.rs                |  42 
crates/editor/src/proposed_changes_editor.rs |   8 
crates/language/src/proto.rs                 |  32 +
crates/lsp/src/lsp.rs                        |   3 
crates/project/src/lsp_command.rs            | 151 ++++
crates/project/src/lsp_store.rs              | 646 ++++++++++++++++++++-
crates/project/src/project.rs                |  25 
crates/proto/proto/lsp.proto                 |  48 +
crates/proto/proto/zed.proto                 |   7 
crates/proto/src/proto.rs                    |   8 
crates/terminal_view/src/terminal_element.rs |   2 
22 files changed, 1,796 insertions(+), 268 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1045,6 +1045,19 @@
   // Automatically update Zed. This setting may be ignored on Linux if
   // installed through a package manager.
   "auto_update": true,
+  // How to render LSP `textDocument/documentColor` colors in the editor.
+  //
+  // Possible values:
+  //
+  // 1. Do not query and render document colors.
+  //      "lsp_document_colors": "none",
+  // 2. Render document colors as inlay hints near the color text (default).
+  //      "lsp_document_colors": "inlay",
+  // 3. Draw a border around the color text.
+  //      "lsp_document_colors": "border",
+  // 4. Draw a background behind the color text..
+  //      "lsp_document_colors": "background",
+  "lsp_document_colors": "inlay",
   // Diagnostics configuration.
   "diagnostics": {
     // Whether to show the project diagnostics button in the status bar.

crates/collab/src/rpc.rs 🔗

@@ -323,6 +323,7 @@ impl Server {
             .add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
             .add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
             .add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
+            .add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
             .add_request_handler(forward_mutating_project_request::<proto::GetCodeLens>)
             .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
             .add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)

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

@@ -4,7 +4,7 @@ use crate::{
 };
 use call::ActiveCall;
 use editor::{
-    Editor, RowInfo,
+    DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
     actions::{
         ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
         ExpandMacroRecursively, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -16,7 +16,7 @@ use editor::{
 };
 use fs::Fs;
 use futures::StreamExt;
-use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
+use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
 use indoc::indoc;
 use language::{
     FakeLspAdapter,
@@ -1951,6 +1951,283 @@ async fn test_inlay_hint_refresh_is_forwarded(
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+    let expected_color = Rgba {
+        r: 0.33,
+        g: 0.33,
+        b: 0.33,
+        a: 0.33,
+    };
+    let mut server = TestServer::start(cx_a.executor()).await;
+    let executor = cx_a.executor();
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    cx_a.update(|cx| {
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.lsp_document_colors = Some(DocumentColorsRenderMode::None);
+            });
+        });
+    });
+    cx_b.update(|cx| {
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+            });
+        });
+    });
+
+    client_a.language_registry().add(rust_lang());
+    client_b.language_registry().add(rust_lang());
+    let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
+        "Rust",
+        FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
+                ..lsp::ServerCapabilities::default()
+            },
+            ..FakeLspAdapter::default()
+        },
+    );
+
+    // Client A opens a project.
+    client_a
+        .fs()
+        .insert_tree(
+            path!("/a"),
+            json!({
+                "main.rs": "fn main() { a }",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    // Client B joins the project
+    let project_b = client_b.join_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+    executor.start_waiting();
+
+    // The host opens a rust file.
+    let _buffer_a = project_a
+        .update(cx_a, |project, cx| {
+            project.open_local_buffer(path!("/a/main.rs"), cx)
+        })
+        .await
+        .unwrap();
+    let editor_a = workspace_a
+        .update_in(cx_a, |workspace, window, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let fake_language_server = fake_language_servers.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::Url::from_file_path(path!("/a/main.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,
+                    },
+                }])
+            }
+        });
+    executor.run_until_parked();
+
+    assert_eq!(
+        0,
+        requests_made.load(atomic::Ordering::Acquire),
+        "Host did not enable document colors, hence should query for none"
+    );
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            Vec::<Rgba>::new(),
+            extract_color_inlays(editor, cx),
+            "No query colors should result in no hints"
+        );
+    });
+
+    let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+    let editor_b = workspace_b
+        .update_in(cx_b, |workspace, window, cx| {
+            workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    color_request_handle.next().await.unwrap();
+    executor.run_until_parked();
+
+    assert_eq!(
+        1,
+        requests_made.load(atomic::Ordering::Acquire),
+        "The client opened the file and got its first colors back"
+    );
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(
+            vec![expected_color],
+            extract_color_inlays(editor, cx),
+            "With document colors as inlays, color inlays should be pushed"
+        );
+    });
+
+    editor_a.update_in(cx_a, |editor, window, cx| {
+        editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
+        editor.handle_input(":", window, cx);
+    });
+    color_request_handle.next().await.unwrap();
+    executor.run_until_parked();
+    assert_eq!(
+        2,
+        requests_made.load(atomic::Ordering::Acquire),
+        "After the host edits his file, the client should request the colors again"
+    );
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            Vec::<Rgba>::new(),
+            extract_color_inlays(editor, cx),
+            "Host has no colors still"
+        );
+    });
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
+    });
+
+    cx_b.update(|_, cx| {
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
+            });
+        });
+    });
+    executor.run_until_parked();
+    assert_eq!(
+        2,
+        requests_made.load(atomic::Ordering::Acquire),
+        "After the client have changed the colors settings, no extra queries should happen"
+    );
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            Vec::<Rgba>::new(),
+            extract_color_inlays(editor, cx),
+            "Host is unaffected by the client's settings changes"
+        );
+    });
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(
+            Vec::<Rgba>::new(),
+            extract_color_inlays(editor, cx),
+            "Client should have no colors hints, as in the settings"
+        );
+    });
+
+    cx_b.update(|_, cx| {
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+            });
+        });
+    });
+    executor.run_until_parked();
+    assert_eq!(
+        2,
+        requests_made.load(atomic::Ordering::Acquire),
+        "After falling back to colors as inlays, no extra LSP queries are made"
+    );
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            Vec::<Rgba>::new(),
+            extract_color_inlays(editor, cx),
+            "Host is unaffected by the client's settings changes, again"
+        );
+    });
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(
+            vec![expected_color],
+            extract_color_inlays(editor, cx),
+            "Client should have its color hints back"
+        );
+    });
+
+    cx_a.update(|_, cx| {
+        SettingsStore::update_global(cx, |store, cx| {
+            store.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
+            });
+        });
+    });
+    color_request_handle.next().await.unwrap();
+    executor.run_until_parked();
+    assert_eq!(
+        3,
+        requests_made.load(atomic::Ordering::Acquire),
+        "After the host enables document colors, another LSP query should be made"
+    );
+    editor_a.update(cx_a, |editor, cx| {
+        assert_eq!(
+            Vec::<Rgba>::new(),
+            extract_color_inlays(editor, cx),
+            "Host did not configure document colors as hints hence gets nothing"
+        );
+    });
+    editor_b.update(cx_b, |editor, cx| {
+        assert_eq!(
+            vec![expected_color],
+            extract_color_inlays(editor, cx),
+            "Client should be unaffected by the host's settings changes"
+        );
+    });
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
     let mut server = TestServer::start(cx_a.executor()).await;
@@ -2834,6 +3111,16 @@ fn extract_hint_labels(editor: &Editor) -> Vec<String> {
     labels
 }
 
+#[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()
+}
+
 fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
     git::blame::BlameEntry {
         sha: sha.parse().unwrap(),

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -1,7 +1,7 @@
 use super::*;
 use collections::{HashMap, HashSet};
 use editor::{
-    DisplayPoint, EditorSettings, InlayId,
+    DisplayPoint, EditorSettings,
     actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
     display_map::{DisplayRow, Inlay},
     test::{
@@ -870,11 +870,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
 
                         editor.splice_inlays(
                             &[],
-                            vec![Inlay {
-                                id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
-                                position: snapshot.buffer_snapshot.anchor_before(position),
-                                text: Rope::from(format!("Test inlay {next_inlay_id}")),
-                            }],
+                            vec![Inlay::inline_completion(
+                                post_inc(&mut next_inlay_id),
+                                snapshot.buffer_snapshot.anchor_before(position),
+                                format!("Test inlay {next_inlay_id}"),
+                            )],
                             cx,
                         );
                     }

crates/editor/src/display_map.rs 🔗

@@ -2014,11 +2014,11 @@ pub mod tests {
         map.update(cx, |map, cx| {
             map.splice_inlays(
                 &[],
-                vec![Inlay {
-                    id: InlayId::InlineCompletion(0),
-                    position: buffer_snapshot.anchor_after(0),
-                    text: "\n".into(),
-                }],
+                vec![Inlay::inline_completion(
+                    0,
+                    buffer_snapshot.anchor_after(0),
+                    "\n",
+                )],
                 cx,
             );
         });

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

@@ -1,5 +1,6 @@
 use crate::{HighlightStyles, InlayId};
 use collections::BTreeSet;
+use gpui::{Hsla, Rgba};
 use language::{Chunk, Edit, Point, TextSummary};
 use multi_buffer::{
     Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset,
@@ -39,6 +40,7 @@ pub struct Inlay {
     pub id: InlayId,
     pub position: Anchor,
     pub text: text::Rope,
+    color: Option<Hsla>,
 }
 
 impl Inlay {
@@ -54,6 +56,26 @@ impl Inlay {
             id: InlayId::Hint(id),
             position,
             text: text.into(),
+            color: None,
+        }
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn mock_hint(id: usize, position: Anchor, text: impl Into<Rope>) -> Self {
+        Self {
+            id: InlayId::Hint(id),
+            position,
+            text: text.into(),
+            color: None,
+        }
+    }
+
+    pub fn color(id: usize, position: Anchor, color: Rgba) -> Self {
+        Self {
+            id: InlayId::Color(id),
+            position,
+            text: Rope::from("◼"),
+            color: Some(Hsla::from(color)),
         }
     }
 
@@ -62,16 +84,23 @@ impl Inlay {
             id: InlayId::InlineCompletion(id),
             position,
             text: text.into(),
+            color: None,
         }
     }
 
-    pub fn debugger_hint<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+    pub fn debugger<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
         Self {
             id: InlayId::DebuggerValue(id),
             position,
             text: text.into(),
+            color: None,
         }
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn get_color(&self) -> Option<Hsla> {
+        self.color
+    }
 }
 
 impl sum_tree::Item for Transform {
@@ -296,6 +325,14 @@ impl<'a> Iterator for InlayChunks<'a> {
                     }
                     InlayId::Hint(_) => self.highlight_styles.inlay_hint,
                     InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
+                    InlayId::Color(_) => match inlay.color {
+                        Some(color) => {
+                            let style = self.highlight_styles.inlay_hint.get_or_insert_default();
+                            style.color = Some(color);
+                            Some(*style)
+                        }
+                        None => self.highlight_styles.inlay_hint,
+                    },
                 };
                 let next_inlay_highlight_endpoint;
                 let offset_in_inlay = self.output_offset - self.transforms.start().0;
@@ -634,24 +671,24 @@ impl InlayMap {
                     .take(len)
                     .collect::<String>();
 
-                let inlay_id = if i % 2 == 0 {
-                    InlayId::Hint(post_inc(next_inlay_id))
+                let next_inlay = if i % 2 == 0 {
+                    Inlay::mock_hint(
+                        post_inc(next_inlay_id),
+                        snapshot.buffer.anchor_at(position, bias),
+                        text.clone(),
+                    )
                 } else {
-                    InlayId::InlineCompletion(post_inc(next_inlay_id))
+                    Inlay::inline_completion(
+                        post_inc(next_inlay_id),
+                        snapshot.buffer.anchor_at(position, bias),
+                        text.clone(),
+                    )
                 };
+                let inlay_id = next_inlay.id;
                 log::info!(
-                    "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
-                    inlay_id,
-                    position,
-                    bias,
-                    text
+                    "creating inlay {inlay_id:?} at buffer offset {position} with bias {bias:?} and text {text:?}"
                 );
-
-                to_insert.push(Inlay {
-                    id: inlay_id,
-                    position: snapshot.buffer.anchor_at(position, bias),
-                    text: text.into(),
-                });
+                to_insert.push(next_inlay);
             } else {
                 to_remove.push(
                     self.inlays
@@ -1183,11 +1220,11 @@ mod tests {
 
         let (inlay_snapshot, _) = inlay_map.splice(
             &[],
-            vec![Inlay {
-                id: InlayId::Hint(post_inc(&mut next_inlay_id)),
-                position: buffer.read(cx).snapshot(cx).anchor_after(3),
-                text: "|123|".into(),
-            }],
+            vec![Inlay::mock_hint(
+                post_inc(&mut next_inlay_id),
+                buffer.read(cx).snapshot(cx).anchor_after(3),
+                "|123|",
+            )],
         );
         assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
         assert_eq!(
@@ -1260,16 +1297,16 @@ mod tests {
         let (inlay_snapshot, _) = inlay_map.splice(
             &[],
             vec![
-                Inlay {
-                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
-                    position: buffer.read(cx).snapshot(cx).anchor_before(3),
-                    text: "|123|".into(),
-                },
-                Inlay {
-                    id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
-                    position: buffer.read(cx).snapshot(cx).anchor_after(3),
-                    text: "|456|".into(),
-                },
+                Inlay::mock_hint(
+                    post_inc(&mut next_inlay_id),
+                    buffer.read(cx).snapshot(cx).anchor_before(3),
+                    "|123|",
+                ),
+                Inlay::inline_completion(
+                    post_inc(&mut next_inlay_id),
+                    buffer.read(cx).snapshot(cx).anchor_after(3),
+                    "|456|",
+                ),
             ],
         );
         assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
@@ -1475,21 +1512,21 @@ mod tests {
         let (inlay_snapshot, _) = inlay_map.splice(
             &[],
             vec![
-                Inlay {
-                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
-                    position: buffer.read(cx).snapshot(cx).anchor_before(0),
-                    text: "|123|\n".into(),
-                },
-                Inlay {
-                    id: InlayId::Hint(post_inc(&mut next_inlay_id)),
-                    position: buffer.read(cx).snapshot(cx).anchor_before(4),
-                    text: "|456|".into(),
-                },
-                Inlay {
-                    id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
-                    position: buffer.read(cx).snapshot(cx).anchor_before(7),
-                    text: "\n|567|\n".into(),
-                },
+                Inlay::mock_hint(
+                    post_inc(&mut next_inlay_id),
+                    buffer.read(cx).snapshot(cx).anchor_before(0),
+                    "|123|\n",
+                ),
+                Inlay::mock_hint(
+                    post_inc(&mut next_inlay_id),
+                    buffer.read(cx).snapshot(cx).anchor_before(4),
+                    "|456|",
+                ),
+                Inlay::inline_completion(
+                    post_inc(&mut next_inlay_id),
+                    buffer.read(cx).snapshot(cx).anchor_before(7),
+                    "\n|567|\n",
+                ),
             ],
         );
         assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");

crates/editor/src/editor.rs 🔗

@@ -29,6 +29,7 @@ mod inlay_hint_cache;
 pub mod items;
 mod jsx_tag_auto_close;
 mod linked_editing_ranges;
+mod lsp_colors;
 mod lsp_ext;
 mod mouse_context_menu;
 pub mod movement;
@@ -63,8 +64,8 @@ use dap::TelemetrySpawnLocation;
 use display_map::*;
 pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
 pub use editor_settings::{
-    CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes,
-    SearchSettings, ShowScrollbar,
+    CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
+    ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowScrollbar,
 };
 use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
 pub use editor_settings_controls::*;
@@ -79,6 +80,7 @@ use futures::{
     stream::FuturesUnordered,
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
+use lsp_colors::LspColorData;
 
 use ::git::blame::BlameEntry;
 use ::git::{Restore, blame::ParsedCommitMessage};
@@ -109,10 +111,9 @@ pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 use language::{
     AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
-    CursorShape, DiagnosticEntry, DiagnosticSourceKind, DiffOptions, DocumentationConfig,
-    EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language,
-    OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
-    WordsQuery,
+    CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
+    EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
+    Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
     language_settings::{
         self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
         all_language_settings, language_settings,
@@ -125,7 +126,7 @@ use markdown::Markdown;
 use mouse_context_menu::MouseContextMenu;
 use persistence::DB;
 use project::{
-    BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath, PulledDiagnostics,
+    BreakpointWithPosition, CompletionResponse, ProjectPath,
     debugger::{
         breakpoint_store::{
             BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
@@ -274,16 +275,19 @@ impl InlineValueCache {
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
 pub enum InlayId {
     InlineCompletion(usize),
-    Hint(usize),
     DebuggerValue(usize),
+    // LSP
+    Hint(usize),
+    Color(usize),
 }
 
 impl InlayId {
     fn id(&self) -> usize {
         match self {
             Self::InlineCompletion(id) => *id,
-            Self::Hint(id) => *id,
             Self::DebuggerValue(id) => *id,
+            Self::Hint(id) => *id,
+            Self::Color(id) => *id,
         }
     }
 }
@@ -1134,6 +1138,8 @@ pub struct Editor {
     inline_value_cache: InlineValueCache,
     selection_drag_state: SelectionDragState,
     drag_and_drop_selection_enabled: bool,
+    next_color_inlay_id: usize,
+    colors: Option<LspColorData>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1795,13 +1801,13 @@ impl Editor {
                             editor
                                 .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
                         }
-                        project::Event::LanguageServerAdded(..)
-                        | project::Event::LanguageServerRemoved(..) => {
+                        project::Event::LanguageServerAdded(server_id, ..)
+                        | project::Event::LanguageServerRemoved(server_id) => {
                             if editor.tasks_update_task.is_none() {
                                 editor.tasks_update_task =
                                     Some(editor.refresh_runnables(window, cx));
                             }
-                            editor.pull_diagnostics(None, window, cx);
+                            editor.update_lsp_data(false, Some(*server_id), None, window, cx);
                         }
                         project::Event::SnippetEdit(id, snippet_edits) => {
                             if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
@@ -2070,6 +2076,8 @@ impl Editor {
             ],
             tasks_update_task: None,
             pull_diagnostics_task: Task::ready(()),
+            colors: None,
+            next_color_inlay_id: 0,
             linked_edit_ranges: Default::default(),
             in_project_search: false,
             previous_search_ranges: None,
@@ -2211,7 +2219,8 @@ impl Editor {
 
             editor.minimap =
                 editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
-            editor.pull_diagnostics(None, window, cx);
+            editor.colors = Some(LspColorData::new(cx));
+            editor.update_lsp_data(false, None, None, window, cx);
         }
 
         editor.report_editor_event("Editor Opened", None, cx);
@@ -4899,6 +4908,15 @@ impl Editor {
             .collect()
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn all_inlays(&self, cx: &App) -> Vec<Inlay> {
+        self.display_map
+            .read(cx)
+            .current_inlays()
+            .cloned()
+            .collect()
+    }
+
     fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
         if self.semantics_provider.is_none() || !self.mode.is_full() {
             return;
@@ -16241,8 +16259,14 @@ impl Editor {
             let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
                 buffers
                     .into_iter()
-                    .flat_map(|buffer| {
-                        Some(project.upgrade()?.pull_diagnostics_for_buffer(buffer, cx))
+                    .filter_map(|buffer| {
+                        project
+                            .update(cx, |project, cx| {
+                                project.lsp_store().update(cx, |lsp_store, cx| {
+                                    lsp_store.pull_diagnostics_for_buffer(buffer, cx)
+                                })
+                            })
+                            .ok()
                     })
                     .collect::<FuturesUnordered<_>>()
             }) else {
@@ -19066,7 +19090,7 @@ impl Editor {
                             .into_iter()
                             .flatten()
                             .for_each(|hint| {
-                                let inlay = Inlay::debugger_hint(
+                                let inlay = Inlay::debugger(
                                     post_inc(&mut editor.next_inlay_id),
                                     Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
                                     hint.text(),
@@ -19117,17 +19141,15 @@ impl Editor {
                                         .register_buffer_with_language_servers(&edited_buffer, cx)
                                 });
                         });
-                        if edited_buffer.read(cx).file().is_some() {
-                            self.pull_diagnostics(
-                                Some(edited_buffer.read(cx).remote_id()),
-                                window,
-                                cx,
-                            );
-                        }
                     }
                 }
                 cx.emit(EditorEvent::BufferEdited);
                 cx.emit(SearchEvent::MatchesInvalidated);
+
+                if let Some(buffer) = edited_buffer {
+                    self.update_lsp_data(true, None, Some(buffer.read(cx).remote_id()), window, cx);
+                }
+
                 if *singleton_buffer_edited {
                     if let Some(buffer) = edited_buffer {
                         if buffer.read(cx).file().is_none() {
@@ -19190,6 +19212,7 @@ impl Editor {
                         .detach();
                     }
                 }
+                self.update_lsp_data(false, None, Some(buffer_id), window, cx);
                 cx.emit(EditorEvent::ExcerptsAdded {
                     buffer: buffer.clone(),
                     predecessor: *predecessor,
@@ -19209,7 +19232,7 @@ impl Editor {
                 cx.emit(EditorEvent::ExcerptsRemoved {
                     ids: ids.clone(),
                     removed_buffer_ids: removed_buffer_ids.clone(),
-                })
+                });
             }
             multi_buffer::Event::ExcerptsEdited {
                 excerpt_ids,
@@ -19220,7 +19243,7 @@ impl Editor {
                 });
                 cx.emit(EditorEvent::ExcerptsEdited {
                     ids: excerpt_ids.clone(),
-                })
+                });
             }
             multi_buffer::Event::ExcerptsExpanded { ids } => {
                 self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
@@ -19366,6 +19389,15 @@ impl Editor {
             }
         }
 
+        if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
+            colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
+        }) {
+            if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() {
+                self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
+            }
+            self.refresh_colors(true, None, None, window, cx);
+        }
+
         cx.notify();
     }
 
@@ -20251,6 +20283,18 @@ impl Editor {
 
         self.read_scroll_position_from_db(item_id, workspace_id, window, cx);
     }
+
+    fn update_lsp_data(
+        &mut self,
+        update_on_edit: bool,
+        for_server_id: Option<LanguageServerId>,
+        for_buffer: Option<BufferId>,
+        window: &mut Window,
+        cx: &mut Context<'_, Self>,
+    ) {
+        self.pull_diagnostics(for_buffer, window, cx);
+        self.refresh_colors(update_on_edit, for_server_id, for_buffer, window, cx);
+    }
 }
 
 fn vim_enabled(cx: &App) -> bool {
@@ -20937,12 +20981,6 @@ pub trait SemanticsProvider {
         new_name: String,
         cx: &mut App,
     ) -> Option<Task<Result<ProjectTransaction>>>;
-
-    fn pull_diagnostics_for_buffer(
-        &self,
-        buffer: Entity<Buffer>,
-        cx: &mut App,
-    ) -> Task<anyhow::Result<()>>;
 }
 
 pub trait CompletionProvider {
@@ -21460,85 +21498,6 @@ impl SemanticsProvider for Entity<Project> {
             project.perform_rename(buffer.clone(), position, new_name, cx)
         }))
     }
-
-    fn pull_diagnostics_for_buffer(
-        &self,
-        buffer: Entity<Buffer>,
-        cx: &mut App,
-    ) -> Task<anyhow::Result<()>> {
-        let diagnostics = self.update(cx, |project, cx| {
-            project
-                .lsp_store()
-                .update(cx, |lsp_store, cx| lsp_store.pull_diagnostics(buffer, cx))
-        });
-        let project = self.clone();
-        cx.spawn(async move |cx| {
-            let diagnostics = diagnostics.await.context("pulling diagnostics")?;
-            project.update(cx, |project, cx| {
-                project.lsp_store().update(cx, |lsp_store, cx| {
-                    for diagnostics_set in diagnostics {
-                        let LspPullDiagnostics::Response {
-                            server_id,
-                            uri,
-                            diagnostics,
-                        } = diagnostics_set
-                        else {
-                            continue;
-                        };
-
-                        let adapter = lsp_store.language_server_adapter_for_id(server_id);
-                        let disk_based_sources = adapter
-                            .as_ref()
-                            .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
-                            .unwrap_or(&[]);
-                        match diagnostics {
-                            PulledDiagnostics::Unchanged { result_id } => {
-                                lsp_store
-                                    .merge_diagnostics(
-                                        server_id,
-                                        lsp::PublishDiagnosticsParams {
-                                            uri: uri.clone(),
-                                            diagnostics: Vec::new(),
-                                            version: None,
-                                        },
-                                        Some(result_id),
-                                        DiagnosticSourceKind::Pulled,
-                                        disk_based_sources,
-                                        |_, _| true,
-                                        cx,
-                                    )
-                                    .log_err();
-                            }
-                            PulledDiagnostics::Changed {
-                                diagnostics,
-                                result_id,
-                            } => {
-                                lsp_store
-                                    .merge_diagnostics(
-                                        server_id,
-                                        lsp::PublishDiagnosticsParams {
-                                            uri: uri.clone(),
-                                            diagnostics,
-                                            version: None,
-                                        },
-                                        result_id,
-                                        DiagnosticSourceKind::Pulled,
-                                        disk_based_sources,
-                                        |old_diagnostic, _| match old_diagnostic.source_kind {
-                                            DiagnosticSourceKind::Pulled => false,
-                                            DiagnosticSourceKind::Other
-                                            | DiagnosticSourceKind::Pushed => true,
-                                        },
-                                        cx,
-                                    )
-                                    .log_err();
-                            }
-                        }
-                    }
-                })
-            })
-        })
-    }
 }
 
 fn inlay_hint_settings(

crates/editor/src/editor_settings.rs 🔗

@@ -50,6 +50,22 @@ pub struct EditorSettings {
     pub diagnostics_max_severity: Option<DiagnosticSeverity>,
     pub inline_code_actions: bool,
     pub drag_and_drop_selection: bool,
+    pub lsp_document_colors: DocumentColorsRenderMode,
+}
+
+/// How to render LSP `textDocument/documentColor` colors in the editor.
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DocumentColorsRenderMode {
+    /// Do not query and render document colors.
+    None,
+    /// Render document colors as inlay hints near the color text.
+    #[default]
+    Inlay,
+    /// Draw a border around the color text.
+    Border,
+    /// Draw a background behind the color text.
+    Background,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -521,6 +537,11 @@ pub struct EditorSettingsContent {
     ///
     /// Default: true
     pub drag_and_drop_selection: Option<bool>,
+
+    /// How to render LSP `textDocument/documentColor` colors in the editor.
+    ///
+    /// Default: [`DocumentColorsRenderMode::Inlay`]
+    pub lsp_document_colors: Option<DocumentColorsRenderMode>,
 }
 
 // Toolbar related settings

crates/editor/src/editor_tests.rs 🔗

@@ -22,8 +22,8 @@ use indoc::indoc;
 use language::{
     BracketPairConfig,
     Capability::ReadWrite,
-    FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
-    Override, Point,
+    DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
+    LanguageName, Override, Point,
     language_settings::{
         AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
         LanguageSettingsContent, LspInsertMode, PrettierSettings,

crates/editor/src/element.rs 🔗

@@ -16,8 +16,9 @@ use crate::{
         ToDisplayPoint,
     },
     editor_settings::{
-        CurrentLineHighlight, DoubleClickInMultibuffer, MinimapThumb, MinimapThumbBorder,
-        ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
+        CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, MinimapThumb,
+        MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap,
+        ShowScrollbar,
     },
     git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
     hover_popover::{
@@ -5676,6 +5677,7 @@ impl EditorElement {
 
                 self.paint_lines_background(layout, window, cx);
                 let invisible_display_ranges = self.paint_highlights(layout, window);
+                self.paint_document_colors(layout, window);
                 self.paint_lines(&invisible_display_ranges, layout, window, cx);
                 self.paint_redactions(layout, window);
                 self.paint_cursors(layout, window, cx);
@@ -5703,6 +5705,7 @@ impl EditorElement {
             for (range, color) in &layout.highlighted_ranges {
                 self.paint_highlighted_range(
                     range.clone(),
+                    true,
                     *color,
                     Pixels::ZERO,
                     line_end_overshoot,
@@ -5717,6 +5720,7 @@ impl EditorElement {
                 for selection in selections.iter() {
                     self.paint_highlighted_range(
                         selection.range.clone(),
+                        true,
                         player_color.selection,
                         corner_radius,
                         corner_radius * 2.,
@@ -5792,6 +5796,7 @@ impl EditorElement {
             for range in layout.redacted_ranges.iter() {
                 self.paint_highlighted_range(
                     range.clone(),
+                    true,
                     redaction_color.into(),
                     Pixels::ZERO,
                     line_end_overshoot,
@@ -5802,6 +5807,48 @@ impl EditorElement {
         });
     }
 
+    fn paint_document_colors(&self, layout: &mut EditorLayout, window: &mut Window) {
+        let Some((colors_render_mode, image_colors)) = &layout.document_colors else {
+            return;
+        };
+        if image_colors.is_empty()
+            || colors_render_mode == &DocumentColorsRenderMode::None
+            || colors_render_mode == &DocumentColorsRenderMode::Inlay
+        {
+            return;
+        }
+
+        let line_end_overshoot = layout.line_end_overshoot();
+
+        for (range, color) in image_colors {
+            match colors_render_mode {
+                DocumentColorsRenderMode::Inlay | DocumentColorsRenderMode::None => return,
+                DocumentColorsRenderMode::Background => {
+                    self.paint_highlighted_range(
+                        range.clone(),
+                        true,
+                        *color,
+                        Pixels::ZERO,
+                        line_end_overshoot,
+                        layout,
+                        window,
+                    );
+                }
+                DocumentColorsRenderMode::Border => {
+                    self.paint_highlighted_range(
+                        range.clone(),
+                        false,
+                        *color,
+                        Pixels::ZERO,
+                        line_end_overshoot,
+                        layout,
+                        window,
+                    );
+                }
+            }
+        }
+    }
+
     fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
         for cursor in &mut layout.visible_cursors {
             cursor.paint(layout.content_origin, window, cx);
@@ -6240,6 +6287,7 @@ impl EditorElement {
     fn paint_highlighted_range(
         &self,
         range: Range<DisplayPoint>,
+        fill: bool,
         color: Hsla,
         corner_radius: Pixels,
         line_end_overshoot: Pixels,
@@ -6290,7 +6338,7 @@ impl EditorElement {
                     .collect(),
             };
 
-            highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
+            highlighted_range.paint(fill, layout.position_map.text_hitbox.bounds, window);
         }
     }
 
@@ -8061,6 +8109,12 @@ impl Element for EditorElement {
                             cx,
                         );
 
+                    let document_colors = self
+                        .editor
+                        .read(cx)
+                        .colors
+                        .as_ref()
+                        .map(|colors| colors.editor_display_highlights(&snapshot));
                     let redacted_ranges = self.editor.read(cx).redacted_ranges(
                         start_anchor..end_anchor,
                         &snapshot.display_snapshot,
@@ -8808,6 +8862,7 @@ impl Element for EditorElement {
                         highlighted_ranges,
                         highlighted_gutter_ranges,
                         redacted_ranges,
+                        document_colors,
                         line_elements,
                         line_numbers,
                         blamed_display_rows,
@@ -9013,6 +9068,7 @@ pub struct EditorLayout {
     tab_invisible: ShapedLine,
     space_invisible: ShapedLine,
     sticky_buffer_header: Option<AnyElement>,
+    document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
 }
 
 impl EditorLayout {
@@ -9735,17 +9791,18 @@ pub struct HighlightedRangeLine {
 }
 
 impl HighlightedRange {
-    pub fn paint(&self, bounds: Bounds<Pixels>, window: &mut Window) {
+    pub fn paint(&self, fill: bool, bounds: Bounds<Pixels>, window: &mut Window) {
         if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
-            self.paint_lines(self.start_y, &self.lines[0..1], bounds, window);
+            self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window);
             self.paint_lines(
                 self.start_y + self.line_height,
                 &self.lines[1..],
+                fill,
                 bounds,
                 window,
             );
         } else {
-            self.paint_lines(self.start_y, &self.lines, bounds, window);
+            self.paint_lines(self.start_y, &self.lines, fill, bounds, window);
         }
     }
 
@@ -9753,6 +9810,7 @@ impl HighlightedRange {
         &self,
         start_y: Pixels,
         lines: &[HighlightedRangeLine],
+        fill: bool,
         _bounds: Bounds<Pixels>,
         window: &mut Window,
     ) {
@@ -9779,7 +9837,11 @@ impl HighlightedRange {
         };
 
         let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
-        let mut builder = gpui::PathBuilder::fill();
+        let mut builder = if fill {
+            gpui::PathBuilder::fill()
+        } else {
+            gpui::PathBuilder::stroke(px(1.))
+        };
         builder.move_to(first_top_right - top_curve_width);
         builder.curve_to(first_top_right + curve_height, first_top_right);
 

crates/editor/src/lsp_colors.rs 🔗

@@ -0,0 +1,358 @@
+use std::{cmp, ops::Range};
+
+use collections::HashMap;
+use futures::future::join_all;
+use gpui::{Hsla, Rgba};
+use language::point_from_lsp;
+use lsp::LanguageServerId;
+use multi_buffer::Anchor;
+use project::DocumentColor;
+use settings::Settings as _;
+use text::{Bias, BufferId, OffsetRangeExt as _};
+use ui::{App, Context, Window};
+use util::post_inc;
+
+use crate::{
+    DisplayPoint, Editor, EditorSettings, EditorSnapshot, InlayId, InlaySplice, RangeToAnchorExt,
+    display_map::Inlay, editor_settings::DocumentColorsRenderMode,
+};
+
+#[derive(Debug)]
+pub(super) struct LspColorData {
+    colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
+    inlay_colors: HashMap<InlayId, usize>,
+    render_mode: DocumentColorsRenderMode,
+}
+
+impl LspColorData {
+    pub fn new(cx: &App) -> Self {
+        Self {
+            colors: Vec::new(),
+            inlay_colors: HashMap::default(),
+            render_mode: EditorSettings::get_global(cx).lsp_document_colors,
+        }
+    }
+
+    pub fn render_mode_updated(
+        &mut self,
+        new_render_mode: DocumentColorsRenderMode,
+    ) -> Option<InlaySplice> {
+        if self.render_mode == new_render_mode {
+            return None;
+        }
+        self.render_mode = new_render_mode;
+        match new_render_mode {
+            DocumentColorsRenderMode::Inlay => Some(InlaySplice {
+                to_remove: Vec::new(),
+                to_insert: self
+                    .colors
+                    .iter()
+                    .map(|(range, color, id)| {
+                        Inlay::color(
+                            id.id(),
+                            range.start,
+                            Rgba {
+                                r: color.color.red,
+                                g: color.color.green,
+                                b: color.color.blue,
+                                a: color.color.alpha,
+                            },
+                        )
+                    })
+                    .collect(),
+            }),
+            DocumentColorsRenderMode::None => {
+                self.colors.clear();
+                Some(InlaySplice {
+                    to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
+                    to_insert: Vec::new(),
+                })
+            }
+            DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
+                Some(InlaySplice {
+                    to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
+                    to_insert: Vec::new(),
+                })
+            }
+        }
+    }
+
+    fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
+        if self.colors == colors {
+            return false;
+        }
+
+        self.inlay_colors = colors
+            .iter()
+            .enumerate()
+            .map(|(i, (_, _, id))| (*id, i))
+            .collect();
+        self.colors = colors;
+        true
+    }
+
+    pub fn editor_display_highlights(
+        &self,
+        snapshot: &EditorSnapshot,
+    ) -> (DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>) {
+        let render_mode = self.render_mode;
+        let highlights = if render_mode == DocumentColorsRenderMode::None
+            || render_mode == DocumentColorsRenderMode::Inlay
+        {
+            Vec::new()
+        } else {
+            self.colors
+                .iter()
+                .map(|(range, color, _)| {
+                    let display_range = range.clone().to_display_points(snapshot);
+                    let color = Hsla::from(Rgba {
+                        r: color.color.red,
+                        g: color.color.green,
+                        b: color.color.blue,
+                        a: color.color.alpha,
+                    });
+                    (display_range, color)
+                })
+                .collect()
+        };
+        (render_mode, highlights)
+    }
+}
+
+impl Editor {
+    pub(super) fn refresh_colors(
+        &mut self,
+        update_on_edit: bool,
+        for_server_id: Option<LanguageServerId>,
+        buffer_id: Option<BufferId>,
+        _: &Window,
+        cx: &mut Context<Self>,
+    ) {
+        if !self.mode().is_full() {
+            return;
+        }
+        let Some(project) = self.project.clone() else {
+            return;
+        };
+        if self
+            .colors
+            .as_ref()
+            .is_none_or(|colors| colors.render_mode == DocumentColorsRenderMode::None)
+        {
+            return;
+        }
+
+        let all_colors_task = project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
+            self.buffer()
+                .update(cx, |multi_buffer, cx| {
+                    multi_buffer
+                        .all_buffers()
+                        .into_iter()
+                        .filter(|editor_buffer| {
+                            buffer_id.is_none_or(|buffer_id| {
+                                buffer_id == editor_buffer.read(cx).remote_id()
+                            })
+                        })
+                        .collect::<Vec<_>>()
+                })
+                .into_iter()
+                .filter_map(|buffer| {
+                    let buffer_id = buffer.read(cx).remote_id();
+                    let colors_task =
+                        lsp_store.document_colors(update_on_edit, for_server_id, buffer, cx)?;
+                    Some(async move { (buffer_id, colors_task.await) })
+                })
+                .collect::<Vec<_>>()
+        });
+        cx.spawn(async move |editor, cx| {
+            let all_colors = join_all(all_colors_task).await;
+            let Ok((multi_buffer_snapshot, editor_excerpts)) = editor.update(cx, |editor, cx| {
+                let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+                let editor_excerpts = multi_buffer_snapshot.excerpts().fold(
+                    HashMap::default(),
+                    |mut acc, (excerpt_id, buffer_snapshot, excerpt_range)| {
+                        let excerpt_data = acc
+                            .entry(buffer_snapshot.remote_id())
+                            .or_insert_with(Vec::new);
+                        let excerpt_point_range =
+                            excerpt_range.context.to_point_utf16(&buffer_snapshot);
+                        excerpt_data.push((
+                            excerpt_id,
+                            buffer_snapshot.clone(),
+                            excerpt_point_range,
+                        ));
+                        acc
+                    },
+                );
+                (multi_buffer_snapshot, editor_excerpts)
+            }) else {
+                return;
+            };
+
+            let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
+            for (buffer_id, colors) in all_colors {
+                let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
+                    continue;
+                };
+                match colors {
+                    Ok(colors) => {
+                        for color in colors {
+                            let color_start = point_from_lsp(color.lsp_range.start);
+                            let color_end = point_from_lsp(color.lsp_range.end);
+
+                            for (excerpt_id, buffer_snapshot, excerpt_range) in excerpts {
+                                if !excerpt_range.contains(&color_start.0)
+                                    || !excerpt_range.contains(&color_end.0)
+                                {
+                                    continue;
+                                }
+                                let Some(color_start_anchor) = multi_buffer_snapshot
+                                    .anchor_in_excerpt(
+                                        *excerpt_id,
+                                        buffer_snapshot.anchor_before(
+                                            buffer_snapshot
+                                                .clip_point_utf16(color_start, Bias::Left),
+                                        ),
+                                    )
+                                else {
+                                    continue;
+                                };
+                                let Some(color_end_anchor) = multi_buffer_snapshot
+                                    .anchor_in_excerpt(
+                                        *excerpt_id,
+                                        buffer_snapshot.anchor_after(
+                                            buffer_snapshot
+                                                .clip_point_utf16(color_end, Bias::Right),
+                                        ),
+                                    )
+                                else {
+                                    continue;
+                                };
+
+                                let (Ok(i) | Err(i)) =
+                                    new_editor_colors.binary_search_by(|(probe, _)| {
+                                        probe
+                                            .start
+                                            .cmp(&color_start_anchor, &multi_buffer_snapshot)
+                                            .then_with(|| {
+                                                probe
+                                                    .end
+                                                    .cmp(&color_end_anchor, &multi_buffer_snapshot)
+                                            })
+                                    });
+                                new_editor_colors
+                                    .insert(i, (color_start_anchor..color_end_anchor, color));
+                                break;
+                            }
+                        }
+                    }
+                    Err(e) => log::error!("Failed to retrieve document colors: {e}"),
+                }
+            }
+
+            editor
+                .update(cx, |editor, cx| {
+                    let mut colors_splice = InlaySplice::default();
+                    let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
+                    let Some(colors) = &mut editor.colors else {
+                        return;
+                    };
+                    let mut existing_colors = colors.colors.iter().peekable();
+                    for (new_range, new_color) in new_editor_colors {
+                        let rgba_color = Rgba {
+                            r: new_color.color.red,
+                            g: new_color.color.green,
+                            b: new_color.color.blue,
+                            a: new_color.color.alpha,
+                        };
+
+                        loop {
+                            match existing_colors.peek() {
+                                Some((existing_range, existing_color, existing_inlay_id)) => {
+                                    match existing_range
+                                        .start
+                                        .cmp(&new_range.start, &multi_buffer_snapshot)
+                                        .then_with(|| {
+                                            existing_range
+                                                .end
+                                                .cmp(&new_range.end, &multi_buffer_snapshot)
+                                        }) {
+                                        cmp::Ordering::Less => {
+                                            colors_splice.to_remove.push(*existing_inlay_id);
+                                            existing_colors.next();
+                                            continue;
+                                        }
+                                        cmp::Ordering::Equal => {
+                                            if existing_color == &new_color {
+                                                new_color_inlays.push((
+                                                    new_range,
+                                                    new_color,
+                                                    *existing_inlay_id,
+                                                ));
+                                            } else {
+                                                colors_splice.to_remove.push(*existing_inlay_id);
+
+                                                let inlay = Inlay::color(
+                                                    post_inc(&mut editor.next_color_inlay_id),
+                                                    new_range.start,
+                                                    rgba_color,
+                                                );
+                                                let inlay_id = inlay.id;
+                                                colors_splice.to_insert.push(inlay);
+                                                new_color_inlays
+                                                    .push((new_range, new_color, inlay_id));
+                                            }
+                                            existing_colors.next();
+                                            break;
+                                        }
+                                        cmp::Ordering::Greater => {
+                                            let inlay = Inlay::color(
+                                                post_inc(&mut editor.next_color_inlay_id),
+                                                new_range.start,
+                                                rgba_color,
+                                            );
+                                            let inlay_id = inlay.id;
+                                            colors_splice.to_insert.push(inlay);
+                                            new_color_inlays.push((new_range, new_color, inlay_id));
+                                            break;
+                                        }
+                                    }
+                                }
+                                None => {
+                                    let inlay = Inlay::color(
+                                        post_inc(&mut editor.next_color_inlay_id),
+                                        new_range.start,
+                                        rgba_color,
+                                    );
+                                    let inlay_id = inlay.id;
+                                    colors_splice.to_insert.push(inlay);
+                                    new_color_inlays.push((new_range, new_color, inlay_id));
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if existing_colors.peek().is_some() {
+                        colors_splice
+                            .to_remove
+                            .extend(existing_colors.map(|(_, _, id)| *id));
+                    }
+
+                    let mut updated = colors.set_colors(new_color_inlays);
+                    if colors.render_mode == DocumentColorsRenderMode::Inlay
+                        && (!colors_splice.to_insert.is_empty()
+                            || !colors_splice.to_remove.is_empty())
+                    {
+                        editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx);
+                        updated = true;
+                    }
+
+                    if updated {
+                        cx.notify();
+                    }
+                })
+                .ok();
+        })
+        .detach();
+    }
+}

crates/editor/src/movement.rs 🔗

@@ -789,7 +789,7 @@ pub fn split_display_range_by_lines(
 mod tests {
     use super::*;
     use crate::{
-        Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
+        Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer,
         display_map::Inlay,
         test::{editor_test_context::EditorTestContext, marked_display_snapshot},
     };
@@ -939,26 +939,26 @@ mod tests {
         let inlays = (0..buffer_snapshot.len())
             .flat_map(|offset| {
                 [
-                    Inlay {
-                        id: InlayId::InlineCompletion(post_inc(&mut id)),
-                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
-                        text: "test".into(),
-                    },
-                    Inlay {
-                        id: InlayId::InlineCompletion(post_inc(&mut id)),
-                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
-                        text: "test".into(),
-                    },
-                    Inlay {
-                        id: InlayId::Hint(post_inc(&mut id)),
-                        position: buffer_snapshot.anchor_at(offset, Bias::Left),
-                        text: "test".into(),
-                    },
-                    Inlay {
-                        id: InlayId::Hint(post_inc(&mut id)),
-                        position: buffer_snapshot.anchor_at(offset, Bias::Right),
-                        text: "test".into(),
-                    },
+                    Inlay::inline_completion(
+                        post_inc(&mut id),
+                        buffer_snapshot.anchor_at(offset, Bias::Left),
+                        "test",
+                    ),
+                    Inlay::inline_completion(
+                        post_inc(&mut id),
+                        buffer_snapshot.anchor_at(offset, Bias::Right),
+                        "test",
+                    ),
+                    Inlay::mock_hint(
+                        post_inc(&mut id),
+                        buffer_snapshot.anchor_at(offset, Bias::Left),
+                        "test",
+                    ),
+                    Inlay::mock_hint(
+                        post_inc(&mut id),
+                        buffer_snapshot.anchor_at(offset, Bias::Right),
+                        "test",
+                    ),
                 ]
             })
             .collect();

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -522,12 +522,4 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
     ) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
         None
     }
-
-    fn pull_diagnostics_for_buffer(
-        &self,
-        _: Entity<Buffer>,
-        _: &mut App,
-    ) -> Task<anyhow::Result<()>> {
-        Task::ready(Ok(()))
-    }
 }

crates/language/src/proto.rs 🔗

@@ -11,6 +11,8 @@ use text::*;
 
 pub use proto::{BufferState, File, Operation};
 
+use super::{point_from_lsp, point_to_lsp};
+
 /// Deserializes a `[text::LineEnding]` from the RPC representation.
 pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
     match message {
@@ -582,3 +584,33 @@ pub fn serialize_version(version: &clock::Global) -> Vec<proto::VectorClockEntry
         })
         .collect()
 }
+
+pub fn serialize_lsp_edit(edit: lsp::TextEdit) -> proto::TextEdit {
+    let start = point_from_lsp(edit.range.start).0;
+    let end = point_from_lsp(edit.range.end).0;
+    proto::TextEdit {
+        new_text: edit.new_text,
+        lsp_range_start: Some(proto::PointUtf16 {
+            row: start.row,
+            column: start.column,
+        }),
+        lsp_range_end: Some(proto::PointUtf16 {
+            row: end.row,
+            column: end.column,
+        }),
+    }
+}
+
+pub fn deserialize_lsp_edit(edit: proto::TextEdit) -> Option<lsp::TextEdit> {
+    let start = edit.lsp_range_start?;
+    let start = PointUtf16::new(start.row, start.column);
+    let end = edit.lsp_range_end?;
+    let end = PointUtf16::new(end.row, end.column);
+    Some(lsp::TextEdit {
+        range: lsp::Range {
+            start: point_to_lsp(start),
+            end: point_to_lsp(end),
+        },
+        new_text: edit.new_text,
+    })
+}

crates/lsp/src/lsp.rs 🔗

@@ -804,6 +804,9 @@ impl LanguageServer {
                         related_document_support: Some(true),
                     })
                     .filter(|_| pull_diagnostics),
+                    color_provider: Some(DocumentColorClientCapabilities {
+                        dynamic_registration: Some(false),
+                    }),
                     ..TextDocumentClientCapabilities::default()
                 }),
                 experimental: Some(json!({

crates/project/src/lsp_command.rs 🔗

@@ -1,11 +1,11 @@
 mod signature_help;
 
 use crate::{
-    CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
-    DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
-    InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
-    LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectTransaction,
-    PulledDiagnostics, ResolveState,
+    CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor,
+    DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint,
+    InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
+    LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse,
+    ProjectTransaction, PulledDiagnostics, ResolveState,
     lsp_store::{LocalLspStore, LspStore},
 };
 use anyhow::{Context as _, Result};
@@ -244,6 +244,9 @@ pub(crate) struct InlayHints {
 #[derive(Debug, Copy, Clone)]
 pub(crate) struct GetCodeLens;
 
+#[derive(Debug, Copy, Clone)]
+pub(crate) struct GetDocumentColor;
+
 impl GetCodeLens {
     pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool {
         capabilities
@@ -4143,6 +4146,144 @@ impl LspCommand for GetDocumentDiagnostics {
     }
 }
 
+#[async_trait(?Send)]
+impl LspCommand for GetDocumentColor {
+    type Response = Vec<DocumentColor>;
+    type LspRequest = lsp::request::DocumentColor;
+    type ProtoRequest = proto::GetDocumentColor;
+
+    fn display_name(&self) -> &str {
+        "Document color"
+    }
+
+    fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
+        server_capabilities
+            .server_capabilities
+            .color_provider
+            .is_some()
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &App,
+    ) -> Result<lsp::DocumentColorParams> {
+        Ok(lsp::DocumentColorParams {
+            text_document: make_text_document_identifier(path)?,
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        })
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Vec<lsp::ColorInformation>,
+        _: Entity<LspStore>,
+        _: Entity<Buffer>,
+        _: LanguageServerId,
+        _: AsyncApp,
+    ) -> Result<Self::Response> {
+        Ok(message
+            .into_iter()
+            .map(|color| DocumentColor {
+                lsp_range: color.range,
+                color: color.color,
+                resolved: false,
+                color_presentations: Vec::new(),
+            })
+            .collect())
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
+        proto::GetDocumentColor {
+            project_id,
+            buffer_id: buffer.remote_id().to_proto(),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        _: Self::ProtoRequest,
+        _: Entity<LspStore>,
+        _: Entity<Buffer>,
+        _: AsyncApp,
+    ) -> Result<Self> {
+        Ok(Self {})
+    }
+
+    fn response_to_proto(
+        response: Self::Response,
+        _: &mut LspStore,
+        _: PeerId,
+        buffer_version: &clock::Global,
+        _: &mut App,
+    ) -> proto::GetDocumentColorResponse {
+        proto::GetDocumentColorResponse {
+            colors: response
+                .into_iter()
+                .map(|color| {
+                    let start = point_from_lsp(color.lsp_range.start).0;
+                    let end = point_from_lsp(color.lsp_range.end).0;
+                    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.row,
+                            column: start.column,
+                        }),
+                        lsp_range_end: Some(proto::PointUtf16 {
+                            row: end.row,
+                            column: end.column,
+                        }),
+                    }
+                })
+                .collect(),
+            version: serialize_version(buffer_version),
+        }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDocumentColorResponse,
+        _: Entity<LspStore>,
+        _: Entity<Buffer>,
+        _: AsyncApp,
+    ) -> Result<Self::Response> {
+        Ok(message
+            .colors
+            .into_iter()
+            .filter_map(|color| {
+                let start = color.lsp_range_start?;
+                let start = PointUtf16::new(start.row, start.column);
+                let end = color.lsp_range_end?;
+                let end = PointUtf16::new(end.row, end.column);
+                Some(DocumentColor {
+                    resolved: false,
+                    color_presentations: Vec::new(),
+                    lsp_range: lsp::Range {
+                        start: point_to_lsp(start),
+                        end: point_to_lsp(end),
+                    },
+                    color: lsp::Color {
+                        red: color.red,
+                        green: color.green,
+                        blue: color.blue,
+                        alpha: color.alpha,
+                    },
+                })
+            })
+            .collect())
+    }
+
+    fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
+        BufferId::new(message.buffer_id)
+    }
+}
+
 fn process_related_documents(
     diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
     server_id: LanguageServerId,

crates/project/src/lsp_store.rs 🔗

@@ -3,9 +3,9 @@ pub mod lsp_ext_command;
 pub mod rust_analyzer_ext;
 
 use crate::{
-    CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
-    LspAction, LspPullDiagnostics, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics,
-    ResolveState, Symbol, ToolchainStore,
+    CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource,
+    CoreCompletion, DocumentColor, Hover, InlayHint, LspAction, LspPullDiagnostics, ProjectItem,
+    ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, ToolchainStore,
     buffer_store::{BufferStore, BufferStoreEvent},
     environment::ProjectEnvironment,
     lsp_command::{self, *},
@@ -24,6 +24,7 @@ use crate::{
 use anyhow::{Context as _, Result, anyhow};
 use async_trait::async_trait;
 use client::{TypedEnvelope, proto};
+use clock::Global;
 use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
 use futures::{
     AsyncWriteExt, Future, FutureExt, StreamExt,
@@ -48,7 +49,10 @@ use language::{
         FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
     },
     point_to_lsp,
-    proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
+    proto::{
+        deserialize_anchor, deserialize_lsp_edit, deserialize_version, serialize_anchor,
+        serialize_lsp_edit, serialize_version,
+    },
     range_from_lsp, range_to_lsp,
 };
 use lsp::{
@@ -320,7 +324,7 @@ impl LocalLspStore {
                             if let Some(lsp_store) = this.upgrade() {
                                 lsp_store
                                     .update(cx, |lsp_store, cx| {
-                                        lsp_store.remove_result_ids(server_id);
+                                        lsp_store.cleanup_lsp_data(server_id);
                                         cx.emit(LspStoreEvent::LanguageServerRemoved(server_id))
                                     })
                                     .ok();
@@ -3481,6 +3485,22 @@ pub struct LspStore {
     _maintain_buffer_languages: Task<()>,
     diagnostic_summaries:
         HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
+    lsp_data: Option<LspData>,
+}
+
+type DocumentColorTask = Shared<Task<std::result::Result<Vec<DocumentColor>, Arc<anyhow::Error>>>>;
+
+#[derive(Debug)]
+struct LspData {
+    mtime: MTime,
+    buffer_lsp_data: HashMap<LanguageServerId, HashMap<PathBuf, BufferLspData>>,
+    colors_update: HashMap<PathBuf, DocumentColorTask>,
+    last_version_queried: HashMap<PathBuf, Global>,
+}
+
+#[derive(Debug, Default)]
+struct BufferLspData {
+    colors: Option<Vec<DocumentColor>>,
 }
 
 pub enum LspStoreEvent {
@@ -3553,6 +3573,7 @@ impl LspStore {
         client.add_entity_request_handler(Self::handle_inlay_hints);
         client.add_entity_request_handler(Self::handle_get_project_symbols);
         client.add_entity_request_handler(Self::handle_resolve_inlay_hint);
+        client.add_entity_request_handler(Self::handle_get_color_presentation);
         client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
         client.add_entity_request_handler(Self::handle_refresh_inlay_hints);
         client.add_entity_request_handler(Self::handle_refresh_code_lens);
@@ -3707,9 +3728,9 @@ impl LspStore {
             languages: languages.clone(),
             language_server_statuses: Default::default(),
             nonce: StdRng::from_entropy().r#gen(),
-            diagnostic_summaries: Default::default(),
+            diagnostic_summaries: HashMap::default(),
+            lsp_data: None,
             active_entry: None,
-
             _maintain_workspace_config,
             _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
         }
@@ -3763,7 +3784,8 @@ impl LspStore {
             languages: languages.clone(),
             language_server_statuses: Default::default(),
             nonce: StdRng::from_entropy().r#gen(),
-            diagnostic_summaries: Default::default(),
+            diagnostic_summaries: HashMap::default(),
+            lsp_data: None,
             active_entry: None,
             toolchain_store,
             _maintain_workspace_config,
@@ -3890,7 +3912,7 @@ impl LspStore {
         cx: &mut Context<Self>,
     ) {
         match event {
-            language::BufferEvent::Edited { .. } => {
+            language::BufferEvent::Edited => {
                 self.on_buffer_edited(buffer, cx);
             }
 
@@ -4835,6 +4857,105 @@ impl LspStore {
         }
     }
 
+    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: 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(crate::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: 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_edit(
         &mut self,
         buffer: &Entity<Buffer>,
@@ -5063,7 +5184,13 @@ impl LspStore {
                 },
                 cx,
             );
-            cx.spawn(async move |_, _| Ok(all_actions_task.await.into_iter().flatten().collect()))
+            cx.spawn(async move |_, _| {
+                Ok(all_actions_task
+                    .await
+                    .into_iter()
+                    .flat_map(|(_, actions)| actions)
+                    .collect())
+            })
         }
     }
 
@@ -5123,7 +5250,13 @@ impl LspStore {
         } else {
             let code_lens_task =
                 self.request_multiple_lsp_locally(buffer_handle, None::<usize>, GetCodeLens, cx);
-            cx.spawn(async move |_, _| Ok(code_lens_task.await.into_iter().flatten().collect()))
+            cx.spawn(async move |_, _| {
+                Ok(code_lens_task
+                    .await
+                    .into_iter()
+                    .flat_map(|(_, code_lens)| code_lens)
+                    .collect())
+            })
         }
     }
 
@@ -5870,6 +6003,293 @@ impl LspStore {
         }
     }
 
+    pub fn pull_diagnostics_for_buffer(
+        &mut self,
+        buffer: Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<()>> {
+        let diagnostics = self.pull_diagnostics(buffer, cx);
+        cx.spawn(async move |lsp_store, cx| {
+            let diagnostics = diagnostics.await.context("pulling diagnostics")?;
+            lsp_store.update(cx, |lsp_store, cx| {
+                for diagnostics_set in diagnostics {
+                    let LspPullDiagnostics::Response {
+                        server_id,
+                        uri,
+                        diagnostics,
+                    } = diagnostics_set
+                    else {
+                        continue;
+                    };
+
+                    let adapter = lsp_store.language_server_adapter_for_id(server_id);
+                    let disk_based_sources = adapter
+                        .as_ref()
+                        .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
+                        .unwrap_or(&[]);
+                    match diagnostics {
+                        PulledDiagnostics::Unchanged { result_id } => {
+                            lsp_store
+                                .merge_diagnostics(
+                                    server_id,
+                                    lsp::PublishDiagnosticsParams {
+                                        uri: uri.clone(),
+                                        diagnostics: Vec::new(),
+                                        version: None,
+                                    },
+                                    Some(result_id),
+                                    DiagnosticSourceKind::Pulled,
+                                    disk_based_sources,
+                                    |_, _| true,
+                                    cx,
+                                )
+                                .log_err();
+                        }
+                        PulledDiagnostics::Changed {
+                            diagnostics,
+                            result_id,
+                        } => {
+                            lsp_store
+                                .merge_diagnostics(
+                                    server_id,
+                                    lsp::PublishDiagnosticsParams {
+                                        uri: uri.clone(),
+                                        diagnostics,
+                                        version: None,
+                                    },
+                                    result_id,
+                                    DiagnosticSourceKind::Pulled,
+                                    disk_based_sources,
+                                    |old_diagnostic, _| match old_diagnostic.source_kind {
+                                        DiagnosticSourceKind::Pulled => false,
+                                        DiagnosticSourceKind::Other
+                                        | DiagnosticSourceKind::Pushed => true,
+                                    },
+                                    cx,
+                                )
+                                .log_err();
+                        }
+                    }
+                }
+            })
+        })
+    }
+
+    pub fn document_colors(
+        &mut self,
+        update_on_edit: bool,
+        for_server_id: Option<LanguageServerId>,
+        buffer: Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Option<DocumentColorTask> {
+        let buffer_mtime = buffer.read(cx).saved_mtime()?;
+        let abs_path = crate::File::from_dyn(buffer.read(cx).file())?.abs_path(cx);
+        let buffer_version = buffer.read(cx).version();
+        let ignore_existing_mtime = update_on_edit
+            && self.lsp_data.as_ref().is_none_or(|lsp_data| {
+                lsp_data.last_version_queried.get(&abs_path) != Some(&buffer_version)
+            });
+
+        let mut has_other_versions = false;
+        let mut received_colors_data = false;
+        let mut outdated_lsp_data = false;
+        let buffer_lsp_data = self
+            .lsp_data
+            .as_ref()
+            .into_iter()
+            .filter(|lsp_data| {
+                if ignore_existing_mtime {
+                    return false;
+                }
+                has_other_versions |= lsp_data.mtime != buffer_mtime;
+                lsp_data.mtime == buffer_mtime
+            })
+            .flat_map(|lsp_data| lsp_data.buffer_lsp_data.values())
+            .filter_map(|buffer_data| buffer_data.get(&abs_path))
+            .filter_map(|buffer_data| {
+                let colors = buffer_data.colors.as_deref()?;
+                received_colors_data = true;
+                Some(colors)
+            })
+            .flatten()
+            .cloned()
+            .collect::<Vec<_>>();
+
+        if buffer_lsp_data.is_empty() || for_server_id.is_some() {
+            if received_colors_data && for_server_id.is_none() {
+                return None;
+            } else if has_other_versions && !ignore_existing_mtime {
+                return None;
+            }
+
+            if ignore_existing_mtime
+                || self.lsp_data.is_none()
+                || self
+                    .lsp_data
+                    .as_ref()
+                    .is_some_and(|lsp_data| buffer_mtime != lsp_data.mtime)
+            {
+                self.lsp_data = Some(LspData {
+                    mtime: buffer_mtime,
+                    buffer_lsp_data: HashMap::default(),
+                    colors_update: HashMap::default(),
+                    last_version_queried: HashMap::default(),
+                });
+                outdated_lsp_data = true;
+            }
+
+            {
+                let lsp_data = self.lsp_data.as_mut()?;
+                match for_server_id {
+                    Some(for_server_id) if !outdated_lsp_data => {
+                        lsp_data.buffer_lsp_data.remove(&for_server_id);
+                    }
+                    None | Some(_) => {
+                        let existing_task = lsp_data.colors_update.get(&abs_path).cloned();
+                        if !outdated_lsp_data && existing_task.is_some() {
+                            return existing_task;
+                        }
+                        for buffer_data in lsp_data.buffer_lsp_data.values_mut() {
+                            if let Some(buffer_data) = buffer_data.get_mut(&abs_path) {
+                                buffer_data.colors = None;
+                            }
+                        }
+                    }
+                }
+            }
+
+            let task_abs_path = abs_path.clone();
+            let new_task = cx
+                .spawn(async move |lsp_store, cx| {
+                    cx.background_executor().timer(Duration::from_millis(50)).await;
+                    let fetched_colors = match lsp_store
+                        .update(cx, |lsp_store, cx| {
+                            lsp_store.fetch_document_colors(buffer, cx)
+                        }) {
+                            Ok(fetch_task) => fetch_task.await
+                            .with_context(|| {
+                                format!(
+                                    "Fetching document colors for buffer with path {task_abs_path:?}"
+                                )
+                            }),
+                            Err(e) => return Err(Arc::new(e)),
+                        };
+                    let fetched_colors = match fetched_colors {
+                        Ok(fetched_colors) => fetched_colors,
+                        Err(e) => return Err(Arc::new(e)),
+                    };
+
+                    let lsp_colors = lsp_store.update(cx, |lsp_store, _| {
+                        let lsp_data = lsp_store.lsp_data.as_mut().with_context(|| format!(
+                            "Document lsp data got updated between fetch and update for path {task_abs_path:?}"
+                        ))?;
+                        let mut lsp_colors = Vec::new();
+                        anyhow::ensure!(lsp_data.mtime == buffer_mtime, "Buffer lsp data got updated between fetch and update for path {task_abs_path:?}");
+                        for (server_id, colors) in fetched_colors {
+                            let colors_lsp_data = &mut lsp_data.buffer_lsp_data.entry(server_id).or_default().entry(task_abs_path.clone()).or_default().colors;
+                            *colors_lsp_data = Some(colors.clone());
+                            lsp_colors.extend(colors);
+                        }
+                        Ok(lsp_colors)
+                    });
+
+                    match lsp_colors {
+                        Ok(Ok(lsp_colors)) => Ok(lsp_colors),
+                        Ok(Err(e)) => Err(Arc::new(e)),
+                        Err(e) => Err(Arc::new(e)),
+                    }
+                })
+                .shared();
+            let lsp_data = self.lsp_data.as_mut()?;
+            lsp_data
+                .colors_update
+                .insert(abs_path.clone(), new_task.clone());
+            lsp_data
+                .last_version_queried
+                .insert(abs_path, buffer_version);
+            Some(new_task)
+        } else {
+            Some(Task::ready(Ok(buffer_lsp_data)).shared())
+        }
+    }
+
+    fn fetch_document_colors(
+        &mut self,
+        buffer: Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Task<anyhow::Result<Vec<(LanguageServerId, Vec<DocumentColor>)>>> {
+        if let Some((client, project_id)) = self.upstream_client() {
+            let request_task = client.request(proto::MultiLspQuery {
+                project_id,
+                buffer_id: buffer.read(cx).remote_id().to_proto(),
+                version: serialize_version(&buffer.read(cx).version()),
+                strategy: Some(proto::multi_lsp_query::Strategy::All(
+                    proto::AllLanguageServers {},
+                )),
+                request: Some(proto::multi_lsp_query::Request::GetDocumentColor(
+                    GetDocumentColor {}.to_proto(project_id, buffer.read(cx)),
+                )),
+            });
+            cx.spawn(async move |project, cx| {
+                let Some(project) = project.upgrade() else {
+                    return Ok(Vec::new());
+                };
+                let colors = join_all(
+                    request_task
+                        .await
+                        .log_err()
+                        .map(|response| response.responses)
+                        .unwrap_or_default()
+                        .into_iter()
+                        .filter_map(|lsp_response| match lsp_response.response? {
+                            proto::lsp_response::Response::GetDocumentColorResponse(response) => {
+                                Some((
+                                    LanguageServerId::from_proto(lsp_response.server_id),
+                                    response,
+                                ))
+                            }
+                            unexpected => {
+                                debug_panic!("Unexpected response: {unexpected:?}");
+                                None
+                            }
+                        })
+                        .map(|(server_id, color_response)| {
+                            let response = GetDocumentColor {}.response_from_proto(
+                                color_response,
+                                project.clone(),
+                                buffer.clone(),
+                                cx.clone(),
+                            );
+                            async move { (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(Vec::new).extend(colors);
+                    acc
+                })
+                .into_iter()
+                .collect();
+                Ok(colors)
+            })
+        } else {
+            let document_colors_task =
+                self.request_multiple_lsp_locally(&buffer, None::<usize>, GetDocumentColor, cx);
+            cx.spawn(async move |_, _| {
+                Ok(document_colors_task
+                    .await
+                    .into_iter()
+                    .fold(HashMap::default(), |mut acc, (server_id, colors)| {
+                        acc.entry(server_id).or_insert_with(Vec::new).extend(colors);
+                        acc
+                    })
+                    .into_iter()
+                    .collect())
+            })
+        }
+    }
+
     pub fn signature_help<T: ToPointUtf16>(
         &mut self,
         buffer: &Entity<Buffer>,
@@ -5937,7 +6357,7 @@ impl LspStore {
                 all_actions_task
                     .await
                     .into_iter()
-                    .flatten()
+                    .flat_map(|(_, actions)| actions)
                     .filter(|help| !help.label.is_empty())
                     .collect::<Vec<_>>()
             })
@@ -6015,7 +6435,7 @@ impl LspStore {
                 all_actions_task
                     .await
                     .into_iter()
-                    .filter_map(|hover| remove_empty_hover_blocks(hover?))
+                    .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?))
                     .collect::<Vec<Hover>>()
             })
         }
@@ -6948,7 +7368,7 @@ impl LspStore {
         position: Option<P>,
         request: R,
         cx: &mut Context<Self>,
-    ) -> Task<Vec<R::Response>>
+    ) -> Task<Vec<(LanguageServerId, R::Response)>>
     where
         P: ToOffset,
         R: LspCommand + Clone,
@@ -6978,20 +7398,21 @@ impl LspStore {
         let mut response_results = server_ids
             .into_iter()
             .map(|server_id| {
-                self.request_lsp(
+                let task = self.request_lsp(
                     buffer.clone(),
                     LanguageServerToQuery::Other(server_id),
                     request.clone(),
                     cx,
-                )
+                );
+                async move { (server_id, task.await) }
             })
             .collect::<FuturesUnordered<_>>();
 
         cx.spawn(async move |_, _| {
             let mut responses = Vec::with_capacity(response_results.len());
-            while let Some(response_result) = response_results.next().await {
+            while let Some((server_id, response_result)) = response_results.next().await {
                 if let Some(response) = response_result.log_err() {
-                    responses.push(response);
+                    responses.push((server_id, response));
                 }
             }
             responses
@@ -7079,9 +7500,14 @@ impl LspStore {
             }
         }
         match envelope.payload.request {
-            Some(proto::multi_lsp_query::Request::GetHover(get_hover)) => {
+            Some(proto::multi_lsp_query::Request::GetHover(message)) => {
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_version(deserialize_version(&message.version))
+                    })?
+                    .await?;
                 let get_hover =
-                    GetHover::from_proto(get_hover, lsp_store.clone(), buffer.clone(), cx.clone())
+                    GetHover::from_proto(message, lsp_store.clone(), buffer.clone(), cx.clone())
                         .await?;
                 let all_hovers = lsp_store
                     .update(&mut cx, |this, cx| {
@@ -7094,10 +7520,13 @@ impl LspStore {
                     })?
                     .await
                     .into_iter()
-                    .filter_map(|hover| remove_empty_hover_blocks(hover?));
+                    .filter_map(|(server_id, hover)| {
+                        Some((server_id, remove_empty_hover_blocks(hover?)?))
+                    });
                 lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
                     responses: all_hovers
-                        .map(|hover| proto::LspResponse {
+                        .map(|(server_id, hover)| proto::LspResponse {
+                            server_id: server_id.to_proto(),
                             response: Some(proto::lsp_response::Response::GetHoverResponse(
                                 GetHover::response_to_proto(
                                     Some(hover),
@@ -7111,9 +7540,14 @@ impl LspStore {
                         .collect(),
                 })
             }
-            Some(proto::multi_lsp_query::Request::GetCodeActions(get_code_actions)) => {
+            Some(proto::multi_lsp_query::Request::GetCodeActions(message)) => {
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_version(deserialize_version(&message.version))
+                    })?
+                    .await?;
                 let get_code_actions = GetCodeActions::from_proto(
-                    get_code_actions,
+                    message,
                     lsp_store.clone(),
                     buffer.clone(),
                     cx.clone(),
@@ -7134,7 +7568,8 @@ impl LspStore {
 
                 lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
                     responses: all_actions
-                        .map(|code_actions| proto::LspResponse {
+                        .map(|(server_id, code_actions)| proto::LspResponse {
+                            server_id: server_id.to_proto(),
                             response: Some(proto::lsp_response::Response::GetCodeActionsResponse(
                                 GetCodeActions::response_to_proto(
                                     code_actions,
@@ -7148,9 +7583,14 @@ impl LspStore {
                         .collect(),
                 })
             }
-            Some(proto::multi_lsp_query::Request::GetSignatureHelp(get_signature_help)) => {
+            Some(proto::multi_lsp_query::Request::GetSignatureHelp(message)) => {
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_version(deserialize_version(&message.version))
+                    })?
+                    .await?;
                 let get_signature_help = GetSignatureHelp::from_proto(
-                    get_signature_help,
+                    message,
                     lsp_store.clone(),
                     buffer.clone(),
                     cx.clone(),
@@ -7171,7 +7611,8 @@ impl LspStore {
 
                 lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
                     responses: all_signatures
-                        .map(|signature_help| proto::LspResponse {
+                        .map(|(server_id, signature_help)| proto::LspResponse {
+                            server_id: server_id.to_proto(),
                             response: Some(
                                 proto::lsp_response::Response::GetSignatureHelpResponse(
                                     GetSignatureHelp::response_to_proto(
@@ -7187,14 +7628,15 @@ impl LspStore {
                         .collect(),
                 })
             }
-            Some(proto::multi_lsp_query::Request::GetCodeLens(get_code_lens)) => {
-                let get_code_lens = GetCodeLens::from_proto(
-                    get_code_lens,
-                    lsp_store.clone(),
-                    buffer.clone(),
-                    cx.clone(),
-                )
-                .await?;
+            Some(proto::multi_lsp_query::Request::GetCodeLens(message)) => {
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_version(deserialize_version(&message.version))
+                    })?
+                    .await?;
+                let get_code_lens =
+                    GetCodeLens::from_proto(message, lsp_store.clone(), buffer.clone(), cx.clone())
+                        .await?;
 
                 let code_lens_actions = lsp_store
                     .update(&mut cx, |project, cx| {
@@ -7210,7 +7652,8 @@ impl LspStore {
 
                 lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
                     responses: code_lens_actions
-                        .map(|actions| proto::LspResponse {
+                        .map(|(server_id, actions)| proto::LspResponse {
+                            server_id: server_id.to_proto(),
                             response: Some(proto::lsp_response::Response::GetCodeLensResponse(
                                 GetCodeLens::response_to_proto(
                                     actions,
@@ -7242,29 +7685,30 @@ impl LspStore {
                         .into_iter()
                         .map(|server_id| {
                             let result_id = lsp_store.result_id(server_id, buffer_id, cx);
-                            lsp_store.request_lsp(
+                            let task = lsp_store.request_lsp(
                                 buffer.clone(),
                                 LanguageServerToQuery::Other(server_id),
                                 GetDocumentDiagnostics {
                                     previous_result_id: result_id,
                                 },
                                 cx,
-                            )
+                            );
+                            async move { (server_id, task.await) }
                         })
                         .collect::<Vec<_>>()
                 })?;
 
                 let all_diagnostics_responses = join_all(pull_diagnostics).await;
                 let mut all_diagnostics = Vec::new();
-                for response in all_diagnostics_responses {
-                    let response = response?;
-                    all_diagnostics.push(response);
+                for (server_id, response) in all_diagnostics_responses {
+                    all_diagnostics.push((server_id, response?));
                 }
 
                 lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
                     responses: all_diagnostics
                         .into_iter()
-                        .map(|lsp_diagnostic| proto::LspResponse {
+                        .map(|(server_id, lsp_diagnostic)| proto::LspResponse {
+                            server_id: server_id.to_proto(),
                             response: Some(
                                 proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
                                     GetDocumentDiagnostics::response_to_proto(
@@ -7280,6 +7724,51 @@ impl LspStore {
                         .collect(),
                 })
             }
+            Some(proto::multi_lsp_query::Request::GetDocumentColor(message)) => {
+                buffer
+                    .update(&mut cx, |buffer, _| {
+                        buffer.wait_for_version(deserialize_version(&message.version))
+                    })?
+                    .await?;
+                let get_document_color = GetDocumentColor::from_proto(
+                    message,
+                    lsp_store.clone(),
+                    buffer.clone(),
+                    cx.clone(),
+                )
+                .await?;
+
+                let all_colors = lsp_store
+                    .update(&mut cx, |project, cx| {
+                        project.request_multiple_lsp_locally(
+                            &buffer,
+                            None::<usize>,
+                            get_document_color,
+                            cx,
+                        )
+                    })?
+                    .await
+                    .into_iter();
+
+                lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
+                    responses: all_colors
+                        .map(|(server_id, colors)| proto::LspResponse {
+                            server_id: server_id.to_proto(),
+                            response: Some(
+                                proto::lsp_response::Response::GetDocumentColorResponse(
+                                    GetDocumentColor::response_to_proto(
+                                        colors,
+                                        project,
+                                        sender_id,
+                                        &buffer_version,
+                                        cx,
+                                    ),
+                                ),
+                            ),
+                        })
+                        .collect(),
+                })
+            }
             None => anyhow::bail!("empty multi lsp query request"),
         }
     }
@@ -8263,6 +8752,70 @@ impl LspStore {
         })
     }
 
+    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,
+                    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(
         this: Entity<Self>,
         envelope: TypedEnvelope<proto::ResolveInlayHint>,
@@ -8829,7 +9382,7 @@ impl LspStore {
         local.language_server_watched_paths.remove(&server_id);
         let server_state = local.language_servers.remove(&server_id);
         cx.notify();
-        self.remove_result_ids(server_id);
+        self.cleanup_lsp_data(server_id);
         cx.emit(LspStoreEvent::LanguageServerRemoved(server_id));
         cx.spawn(async move |_, cx| {
             Self::shutdown_language_server(server_state, name, cx).await;
@@ -9718,7 +10271,10 @@ impl LspStore {
         }
     }
 
-    fn remove_result_ids(&mut self, for_server: LanguageServerId) {
+    fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) {
+        if let Some(lsp_data) = &mut self.lsp_data {
+            lsp_data.buffer_lsp_data.remove(&for_server);
+        }
         if let Some(local) = self.as_local_mut() {
             local.buffer_pull_diagnostics_result_ids.remove(&for_server);
         }

crates/project/src/project.rs 🔗

@@ -768,6 +768,21 @@ pub struct DirectoryItem {
     pub is_dir: bool,
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct DocumentColor {
+    pub lsp_range: lsp::Range,
+    pub color: lsp::Color,
+    pub resolved: bool,
+    pub color_presentations: Vec<ColorPresentation>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ColorPresentation {
+    pub label: String,
+    pub text_edit: Option<lsp::TextEdit>,
+    pub additional_text_edits: Vec<lsp::TextEdit>,
+}
+
 #[derive(Clone)]
 pub enum DirectoryLister {
     Project(Entity<Project>),
@@ -3721,16 +3736,6 @@ impl Project {
         })
     }
 
-    pub fn document_diagnostics(
-        &mut self,
-        buffer_handle: Entity<Buffer>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<LspPullDiagnostics>>> {
-        self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.pull_diagnostics(buffer_handle, cx)
-        })
-    }
-
     pub fn update_diagnostics(
         &mut self,
         language_server_id: LanguageServerId,

crates/proto/proto/lsp.proto 🔗

@@ -666,6 +666,51 @@ message LanguageServerPromptResponse {
     optional uint64 action_response = 1;
 }
 
+message GetDocumentColor {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    repeated VectorClockEntry version = 3;
+
+}
+
+message GetDocumentColorResponse {
+    repeated ColorInformation colors = 1;
+    repeated VectorClockEntry version = 2;
+
+}
+
+message ColorInformation {
+    PointUtf16 lsp_range_start = 1;
+    PointUtf16 lsp_range_end = 2;
+    float red = 3;
+    float green = 4;
+    float blue = 5;
+    float alpha = 6;
+}
+
+message GetColorPresentation {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+    ColorInformation color = 3;
+    uint64 server_id = 4;
+}
+
+message GetColorPresentationResponse {
+    repeated ColorPresentation presentations = 1;
+}
+
+message ColorPresentation {
+    string label = 1;
+    optional TextEdit text_edit = 2;
+    repeated TextEdit additional_text_edits = 3;
+}
+
+message TextEdit {
+    string new_text = 1;
+    PointUtf16 lsp_range_start = 2;
+    PointUtf16 lsp_range_end = 3;
+}
+
 message MultiLspQuery {
     uint64 project_id = 1;
     uint64 buffer_id = 2;
@@ -679,6 +724,7 @@ message MultiLspQuery {
         GetSignatureHelp get_signature_help = 7;
         GetCodeLens get_code_lens = 8;
         GetDocumentDiagnostics get_document_diagnostics = 9;
+        GetDocumentColor get_document_color = 10;
     }
 }
 
@@ -705,7 +751,9 @@ message LspResponse {
         GetSignatureHelpResponse get_signature_help_response = 3;
         GetCodeLensResponse get_code_lens_response = 4;
         GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
+        GetDocumentColorResponse get_document_color_response = 6;
     }
+    uint64 server_id = 7;
 }
 
 message LanguageServerIdForName {

crates/proto/proto/zed.proto 🔗

@@ -391,7 +391,12 @@ message Envelope {
 
         GetDocumentDiagnostics get_document_diagnostics = 350;
         GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351;
-        PullWorkspaceDiagnostics pull_workspace_diagnostics = 352; // current max
+        PullWorkspaceDiagnostics pull_workspace_diagnostics = 352;
+
+        GetDocumentColor get_document_color = 353;
+        GetDocumentColorResponse get_document_color_response = 354;
+        GetColorPresentation get_color_presentation = 355;
+        GetColorPresentationResponse get_color_presentation_response = 356; // current max
 
     }
 

crates/proto/src/proto.rs 🔗

@@ -221,6 +221,10 @@ messages!(
     (ResolveCompletionDocumentationResponse, Background),
     (ResolveInlayHint, Background),
     (ResolveInlayHintResponse, Background),
+    (GetDocumentColor, Background),
+    (GetDocumentColorResponse, Background),
+    (GetColorPresentation, Background),
+    (GetColorPresentationResponse, Background),
     (RefreshCodeLens, Background),
     (GetCodeLens, Background),
     (GetCodeLensResponse, Background),
@@ -400,6 +404,8 @@ request_messages!(
         ResolveCompletionDocumentationResponse
     ),
     (ResolveInlayHint, ResolveInlayHintResponse),
+    (GetDocumentColor, GetDocumentColorResponse),
+    (GetColorPresentation, GetColorPresentationResponse),
     (RespondToChannelInvite, Ack),
     (RespondToContactRequest, Ack),
     (SaveBuffer, BufferSaved),
@@ -487,9 +493,11 @@ entity_messages!(
     BufferSaved,
     CloseBuffer,
     Commit,
+    GetColorPresentation,
     CopyProjectEntry,
     CreateBufferForPeer,
     CreateProjectEntry,
+    GetDocumentColor,
     DeleteProjectEntry,
     ExpandProjectEntry,
     ExpandAllForProjectEntry,

crates/terminal_view/src/terminal_element.rs 🔗

@@ -1072,7 +1072,7 @@ impl Element for TerminalElement {
                                 color: *color,
                                 corner_radius: 0.15 * layout.dimensions.line_height,
                             };
-                            hr.paint(bounds, window);
+                            hr.paint(true, bounds, window);
                         }
                     }