Show document highlights from the language server when moving the cursor

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs         |  75 ++++++++++++++
crates/project/src/lsp_command.rs   | 144 ++++++++++++++++++++++++++++
crates/project/src/project.rs       |  19 +++
crates/rpc/proto/zed.proto          | 126 ++++++++++++++----------
crates/rpc/src/proto.rs             |   4 
crates/server/src/rpc.rs            | 157 +++++++++++++++++++++++++++++++
crates/theme/src/theme.rs           |   4 
crates/zed/assets/themes/_base.toml |   2 
8 files changed, 477 insertions(+), 54 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -442,6 +442,7 @@ pub struct Editor {
     next_completion_id: CompletionId,
     available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
     code_actions_task: Option<Task<()>>,
+    document_highlights_task: Option<Task<()>>,
     pending_rename: Option<RenameState>,
 }
 
@@ -903,6 +904,7 @@ impl Editor {
             next_completion_id: 0,
             available_code_actions: Default::default(),
             code_actions_task: Default::default(),
+            document_highlights_task: Default::default(),
             pending_rename: Default::default(),
         };
         this.end_selection(cx);
@@ -2295,6 +2297,76 @@ impl Editor {
         None
     }
 
+    fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+        let project = self.project.as_ref()?;
+        let buffer = self.buffer.read(cx);
+        let newest_selection = self.newest_anchor_selection().clone();
+        let cursor_position = newest_selection.head();
+        let (cursor_buffer, cursor_buffer_position) =
+            buffer.text_anchor_for_position(cursor_position.clone(), cx)?;
+        let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
+        if cursor_buffer != tail_buffer {
+            return None;
+        }
+
+        let highlights = project.update(cx, |project, cx| {
+            project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
+        });
+
+        enum DocumentHighlightRead {}
+        enum DocumentHighlightWrite {}
+
+        self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move {
+            let highlights = highlights.log_err().await;
+            if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) {
+                this.update(&mut cx, |this, cx| {
+                    let buffer_id = cursor_position.buffer_id;
+                    let excerpt_id = cursor_position.excerpt_id.clone();
+                    let settings = (this.build_settings)(cx);
+                    let buffer = this.buffer.read(cx);
+                    if !buffer
+                        .text_anchor_for_position(cursor_position, cx)
+                        .map_or(false, |(buffer, _)| buffer == cursor_buffer)
+                    {
+                        return;
+                    }
+
+                    let mut write_ranges = Vec::new();
+                    let mut read_ranges = Vec::new();
+                    for highlight in highlights {
+                        let range = Anchor {
+                            buffer_id,
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: highlight.range.start,
+                        }..Anchor {
+                            buffer_id,
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: highlight.range.end,
+                        };
+                        if highlight.kind == lsp::DocumentHighlightKind::WRITE {
+                            write_ranges.push(range);
+                        } else {
+                            read_ranges.push(range);
+                        }
+                    }
+
+                    this.highlight_ranges::<DocumentHighlightRead>(
+                        read_ranges,
+                        settings.style.document_highlight_read_background,
+                        cx,
+                    );
+                    this.highlight_ranges::<DocumentHighlightWrite>(
+                        write_ranges,
+                        settings.style.document_highlight_write_background,
+                        cx,
+                    );
+                    cx.notify();
+                });
+            }
+        }));
+        None
+    }
+
     pub fn render_code_actions_indicator(&self, cx: &mut ViewContext<Self>) -> Option<ElementBox> {
         if self.available_code_actions.is_some() {
             enum Tag {}
@@ -4840,6 +4912,7 @@ impl Editor {
             self.available_code_actions.take();
         }
         self.refresh_code_actions(cx);
+        self.refresh_document_highlights(cx);
 
         self.pause_cursor_blinking(cx);
         cx.emit(Event::SelectionsChanged);
@@ -5294,6 +5367,8 @@ impl EditorSettings {
                     highlighted_line_background: Default::default(),
                     diff_background_deleted: Default::default(),
                     diff_background_inserted: Default::default(),
+                    document_highlight_read_background: Default::default(),
+                    document_highlight_write_background: Default::default(),
                     line_number: Default::default(),
                     line_number_active: Default::default(),
                     selection: Default::default(),

crates/project/src/lsp_command.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Location, Project, ProjectTransaction};
+use crate::{DocumentHighlight, Location, Project, ProjectTransaction};
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use client::{proto, PeerId};
@@ -8,7 +8,8 @@ use language::{
     proto::{deserialize_anchor, serialize_anchor},
     range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16,
 };
-use std::{ops::Range, path::Path};
+use lsp::DocumentHighlightKind;
+use std::{cmp::Reverse, ops::Range, path::Path};
 
 #[async_trait(?Send)]
 pub(crate) trait LspCommand: 'static + Sized {
@@ -70,6 +71,10 @@ pub(crate) struct GetReferences {
     pub position: PointUtf16,
 }
 
+pub(crate) struct GetDocumentHighlights {
+    pub position: PointUtf16,
+}
+
 #[async_trait(?Send)]
 impl LspCommand for PrepareRename {
     type Response = Option<Range<Anchor>>;
@@ -598,3 +603,138 @@ impl LspCommand for GetReferences {
         message.buffer_id
     }
 }
+
+#[async_trait(?Send)]
+impl LspCommand for GetDocumentHighlights {
+    type Response = Vec<DocumentHighlight>;
+    type LspRequest = lsp::request::DocumentHighlightRequest;
+    type ProtoRequest = proto::GetDocumentHighlights;
+
+    fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::DocumentHighlightParams {
+        lsp::DocumentHighlightParams {
+            text_document_position_params: lsp::TextDocumentPositionParams {
+                text_document: lsp::TextDocumentIdentifier {
+                    uri: lsp::Url::from_file_path(path).unwrap(),
+                },
+                position: self.position.to_lsp_position(),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        lsp_highlights: Option<Vec<lsp::DocumentHighlight>>,
+        _: ModelHandle<Project>,
+        buffer: ModelHandle<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<DocumentHighlight>> {
+        buffer.read_with(&cx, |buffer, _| {
+            let mut lsp_highlights = lsp_highlights.unwrap_or_default();
+            lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end)));
+            Ok(lsp_highlights
+                .into_iter()
+                .map(|lsp_highlight| {
+                    let start = buffer
+                        .clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left);
+                    let end = buffer
+                        .clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left);
+                    DocumentHighlight {
+                        range: buffer.anchor_after(start)..buffer.anchor_before(end),
+                        kind: lsp_highlight
+                            .kind
+                            .unwrap_or(lsp::DocumentHighlightKind::READ),
+                    }
+                })
+                .collect())
+        })
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights {
+        proto::GetDocumentHighlights {
+            project_id,
+            buffer_id: buffer.remote_id(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+        }
+    }
+
+    fn from_proto(
+        message: proto::GetDocumentHighlights,
+        _: &mut Project,
+        buffer: &Buffer,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        if !buffer.can_resolve(&position) {
+            Err(anyhow!("cannot resolve position"))?;
+        }
+        Ok(Self {
+            position: position.to_point_utf16(buffer),
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<DocumentHighlight>,
+        _: &mut Project,
+        _: PeerId,
+        _: &clock::Global,
+        _: &AppContext,
+    ) -> proto::GetDocumentHighlightsResponse {
+        let highlights = response
+            .into_iter()
+            .map(|highlight| proto::DocumentHighlight {
+                start: Some(serialize_anchor(&highlight.range.start)),
+                end: Some(serialize_anchor(&highlight.range.end)),
+                kind: match highlight.kind {
+                    DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(),
+                    DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(),
+                    DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(),
+                    _ => proto::document_highlight::Kind::Text.into(),
+                },
+            })
+            .collect();
+        proto::GetDocumentHighlightsResponse { highlights }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDocumentHighlightsResponse,
+        _: ModelHandle<Project>,
+        _: ModelHandle<Buffer>,
+        _: AsyncAppContext,
+    ) -> Result<Vec<DocumentHighlight>> {
+        Ok(message
+            .highlights
+            .into_iter()
+            .map(|highlight| {
+                let start = highlight
+                    .start
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing target start"))?;
+                let end = highlight
+                    .end
+                    .and_then(deserialize_anchor)
+                    .ok_or_else(|| anyhow!("missing target end"))?;
+                let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
+                    Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
+                    Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ,
+                    Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE,
+                    None => DocumentHighlightKind::TEXT,
+                };
+                Ok(DocumentHighlight {
+                    range: start..end,
+                    kind,
+                })
+            })
+            .collect::<Result<Vec<_>>>()?)
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> u64 {
+        message.buffer_id
+    }
+}

crates/project/src/project.rs 🔗

@@ -18,7 +18,7 @@ use language::{
     Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16,
     ToLspPosition, ToOffset, ToPointUtf16, Transaction,
 };
-use lsp::{DiagnosticSeverity, LanguageServer};
+use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer};
 use lsp_command::*;
 use postage::{broadcast, prelude::Stream, sink::Sink, watch};
 use rand::prelude::*;
@@ -123,6 +123,12 @@ pub struct Location {
     pub range: Range<language::Anchor>,
 }
 
+#[derive(Debug)]
+pub struct DocumentHighlight {
+    pub range: Range<language::Anchor>,
+    pub kind: DocumentHighlightKind,
+}
+
 #[derive(Clone, Debug)]
 pub struct Symbol {
     pub source_worktree_id: WorktreeId,
@@ -202,6 +208,7 @@ impl Project {
         client.add_entity_request_handler(Self::handle_get_code_actions);
         client.add_entity_request_handler(Self::handle_get_completions);
         client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
         client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
         client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
         client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
@@ -1269,6 +1276,16 @@ impl Project {
         self.request_lsp(buffer.clone(), GetReferences { position }, cx)
     }
 
+    pub fn document_highlights<T: ToPointUtf16>(
+        &self,
+        buffer: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<DocumentHighlight>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.request_lsp(buffer.clone(), GetDocumentHighlights { position }, cx)
+    }
+
     pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         if self.is_local() {
             let mut language_servers = HashMap::default();

crates/rpc/proto/zed.proto 🔗

@@ -25,57 +25,59 @@ message Envelope {
         GetDefinitionResponse get_definition_response = 19;
         GetReferences get_references = 20;
         GetReferencesResponse get_references_response = 21;
-        GetProjectSymbols get_project_symbols = 22;
-        GetProjectSymbolsResponse get_project_symbols_response = 23;
-        OpenBufferForSymbol open_buffer_for_symbol = 24;
-        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 25;
-
-        RegisterWorktree register_worktree = 26;
-        UnregisterWorktree unregister_worktree = 27;
-        ShareWorktree share_worktree = 28;
-        UpdateWorktree update_worktree = 29;
-        UpdateDiagnosticSummary update_diagnostic_summary = 30;
-        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 31;
-        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 32;
-
-        OpenBuffer open_buffer = 33;
-        OpenBufferResponse open_buffer_response = 34;
-        CloseBuffer close_buffer = 35;
-        UpdateBuffer update_buffer = 36;
-        UpdateBufferFile update_buffer_file = 37;
-        SaveBuffer save_buffer = 38;
-        BufferSaved buffer_saved = 39;
-        BufferReloaded buffer_reloaded = 40;
-        FormatBuffers format_buffers = 41;
-        FormatBuffersResponse format_buffers_response = 42;
-        GetCompletions get_completions = 43;
-        GetCompletionsResponse get_completions_response = 44;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 45;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 46;
-        GetCodeActions get_code_actions = 47;
-        GetCodeActionsResponse get_code_actions_response = 48;
-        ApplyCodeAction apply_code_action = 49;
-        ApplyCodeActionResponse apply_code_action_response = 50;
-        PrepareRename prepare_rename = 51;
-        PrepareRenameResponse prepare_rename_response = 52;
-        PerformRename perform_rename = 53;
-        PerformRenameResponse perform_rename_response = 54;
-
-        GetChannels get_channels = 55;
-        GetChannelsResponse get_channels_response = 56;
-        JoinChannel join_channel = 57;
-        JoinChannelResponse join_channel_response = 58;
-        LeaveChannel leave_channel = 59;
-        SendChannelMessage send_channel_message = 60;
-        SendChannelMessageResponse send_channel_message_response = 61;
-        ChannelMessageSent channel_message_sent = 62;
-        GetChannelMessages get_channel_messages = 63;
-        GetChannelMessagesResponse get_channel_messages_response = 64;
-
-        UpdateContacts update_contacts = 65;
-
-        GetUsers get_users = 66;
-        GetUsersResponse get_users_response = 67;
+        GetDocumentHighlights get_document_highlights = 22;
+        GetDocumentHighlightsResponse get_document_highlights_response = 23;
+        GetProjectSymbols get_project_symbols = 24;
+        GetProjectSymbolsResponse get_project_symbols_response = 25;
+        OpenBufferForSymbol open_buffer_for_symbol = 26;
+        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 27;
+
+        RegisterWorktree register_worktree = 28;
+        UnregisterWorktree unregister_worktree = 29;
+        ShareWorktree share_worktree = 30;
+        UpdateWorktree update_worktree = 31;
+        UpdateDiagnosticSummary update_diagnostic_summary = 32;
+        DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 33;
+        DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 34;
+
+        OpenBuffer open_buffer = 35;
+        OpenBufferResponse open_buffer_response = 36;
+        CloseBuffer close_buffer = 37;
+        UpdateBuffer update_buffer = 38;
+        UpdateBufferFile update_buffer_file = 39;
+        SaveBuffer save_buffer = 40;
+        BufferSaved buffer_saved = 41;
+        BufferReloaded buffer_reloaded = 42;
+        FormatBuffers format_buffers = 43;
+        FormatBuffersResponse format_buffers_response = 44;
+        GetCompletions get_completions = 45;
+        GetCompletionsResponse get_completions_response = 46;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 47;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 48;
+        GetCodeActions get_code_actions = 49;
+        GetCodeActionsResponse get_code_actions_response = 50;
+        ApplyCodeAction apply_code_action = 51;
+        ApplyCodeActionResponse apply_code_action_response = 52;
+        PrepareRename prepare_rename = 53;
+        PrepareRenameResponse prepare_rename_response = 54;
+        PerformRename perform_rename = 55;
+        PerformRenameResponse perform_rename_response = 56;
+
+        GetChannels get_channels = 57;
+        GetChannelsResponse get_channels_response = 58;
+        JoinChannel join_channel = 59;
+        JoinChannelResponse join_channel_response = 60;
+        LeaveChannel leave_channel = 61;
+        SendChannelMessage send_channel_message = 62;
+        SendChannelMessageResponse send_channel_message_response = 63;
+        ChannelMessageSent channel_message_sent = 64;
+        GetChannelMessages get_channel_messages = 65;
+        GetChannelMessagesResponse get_channel_messages_response = 66;
+
+        UpdateContacts update_contacts = 67;
+
+        GetUsers get_users = 68;
+        GetUsersResponse get_users_response = 69;
     }
 }
 
@@ -181,12 +183,34 @@ message GetReferencesResponse {
     repeated Location locations = 1;
 }
 
+message GetDocumentHighlights {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+ }
+
+message GetDocumentHighlightsResponse {
+    repeated DocumentHighlight highlights = 1;
+}
+
 message Location {
     Buffer buffer = 1;
     Anchor start = 2;
     Anchor end = 3;
 }
 
+message DocumentHighlight {
+    Kind kind = 1;
+    Anchor start = 2;
+    Anchor end = 3;
+
+    enum Kind {
+        Text = 0;
+        Read = 1;
+        Write = 2;
+    }
+}
+
 message GetProjectSymbols {
     uint64 project_id = 1;
     string query = 2;

crates/rpc/src/proto.rs 🔗

@@ -157,6 +157,8 @@ messages!(
     (GetCompletionsResponse, Foreground),
     (GetDefinition, Foreground),
     (GetDefinitionResponse, Foreground),
+    (GetDocumentHighlights, Foreground),
+    (GetDocumentHighlightsResponse, Foreground),
     (GetReferences, Foreground),
     (GetReferencesResponse, Foreground),
     (GetProjectSymbols, Background),
@@ -210,6 +212,7 @@ request_messages!(
     (GetCodeActions, GetCodeActionsResponse),
     (GetCompletions, GetCompletionsResponse),
     (GetDefinition, GetDefinitionResponse),
+    (GetDocumentHighlights, GetDocumentHighlightsResponse),
     (GetReferences, GetReferencesResponse),
     (GetProjectSymbols, GetProjectSymbolsResponse),
     (GetUsers, GetUsersResponse),
@@ -245,6 +248,7 @@ entity_messages!(
     GetCodeActions,
     GetCompletions,
     GetDefinition,
+    GetDocumentHighlights,
     GetReferences,
     GetProjectSymbols,
     JoinProject,

crates/server/src/rpc.rs 🔗

@@ -80,6 +80,7 @@ impl Server {
             .add_message_handler(Server::disk_based_diagnostics_updated)
             .add_request_handler(Server::get_definition)
             .add_request_handler(Server::get_references)
+            .add_request_handler(Server::get_document_highlights)
             .add_request_handler(Server::get_project_symbols)
             .add_request_handler(Server::open_buffer_for_symbol)
             .add_request_handler(Server::open_buffer)
@@ -604,6 +605,20 @@ impl Server {
             .await?)
     }
 
+    async fn get_document_highlights(
+        self: Arc<Server>,
+        request: TypedEnvelope<proto::GetDocumentHighlights>,
+    ) -> tide::Result<proto::GetDocumentHighlightsResponse> {
+        let host_connection_id = self
+            .state()
+            .read_project(request.payload.project_id, request.sender_id)?
+            .host_connection_id;
+        Ok(self
+            .peer
+            .forward_request(request.sender_id, host_connection_id, request.payload)
+            .await?)
+    }
+
     async fn get_project_symbols(
         self: Arc<Server>,
         request: TypedEnvelope<proto::GetProjectSymbols>,
@@ -2855,6 +2870,148 @@ mod tests {
         });
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_document_highlights(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let lang_registry = Arc::new(LanguageRegistry::new());
+        let fs = FakeFs::new(cx_a.background());
+        fs.insert_tree(
+            "/root-1",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "main.rs": "fn double(number: i32) -> i32 { number + number }",
+            }),
+        )
+        .await;
+
+        // Set up a fake language server.
+        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
+        lang_registry.add(Arc::new(Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                language_server: Some(language_server_config),
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        )));
+
+        // Connect to a server as 2 clients.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let client_a = server.create_client(&mut cx_a, "user_a").await;
+        let client_b = server.create_client(&mut cx_b, "user_b").await;
+
+        // Share a project as client A
+        let project_a = cx_a.update(|cx| {
+            Project::local(
+                client_a.clone(),
+                client_a.user_store.clone(),
+                lang_registry.clone(),
+                fs.clone(),
+                cx,
+            )
+        });
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_local_worktree("/root-1", false, cx)
+            })
+            .await
+            .unwrap();
+        worktree_a
+            .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
+            .await;
+        let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await;
+        let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id());
+        project_a
+            .update(&mut cx_a, |p, cx| p.share(cx))
+            .await
+            .unwrap();
+
+        // Join the worktree as client B.
+        let project_b = Project::remote(
+            project_id,
+            client_b.clone(),
+            client_b.user_store.clone(),
+            lang_registry.clone(),
+            fs.clone(),
+            &mut cx_b.to_async(),
+        )
+        .await
+        .unwrap();
+
+        // Open the file on client B.
+        let buffer_b = cx_b
+            .background()
+            .spawn(project_b.update(&mut cx_b, |p, cx| {
+                p.open_buffer((worktree_id, "main.rs"), cx)
+            }))
+            .await
+            .unwrap();
+
+        // Request document highlights as the guest.
+        let highlights =
+            project_b.update(&mut cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx));
+
+        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _>(
+            |params| {
+                assert_eq!(
+                    params
+                        .text_document_position_params
+                        .text_document
+                        .uri
+                        .as_str(),
+                    "file:///root-1/main.rs"
+                );
+                assert_eq!(
+                    params.text_document_position_params.position,
+                    lsp::Position::new(0, 34)
+                );
+                Some(vec![
+                    lsp::DocumentHighlight {
+                        kind: Some(lsp::DocumentHighlightKind::WRITE),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 10),
+                            lsp::Position::new(0, 16),
+                        ),
+                    },
+                    lsp::DocumentHighlight {
+                        kind: Some(lsp::DocumentHighlightKind::READ),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 32),
+                            lsp::Position::new(0, 38),
+                        ),
+                    },
+                    lsp::DocumentHighlight {
+                        kind: Some(lsp::DocumentHighlightKind::READ),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 41),
+                            lsp::Position::new(0, 47),
+                        ),
+                    },
+                ])
+            },
+        );
+
+        let highlights = highlights.await.unwrap();
+        buffer_b.read_with(&cx_b, |buffer, _| {
+            let snapshot = buffer.snapshot();
+
+            let highlights = highlights
+                .into_iter()
+                .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
+                .collect::<Vec<_>>();
+            assert_eq!(
+                highlights,
+                &[
+                    (lsp::DocumentHighlightKind::WRITE, 10..16),
+                    (lsp::DocumentHighlightKind::READ, 32..38),
+                    (lsp::DocumentHighlightKind::READ, 41..47)
+                ]
+            )
+        });
+    }
+
     #[gpui::test(iterations = 10)]
     async fn test_project_symbols(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
         cx_a.foreground().forbid_parking();

crates/theme/src/theme.rs 🔗

@@ -279,6 +279,8 @@ pub struct EditorStyle {
     pub gutter_padding_factor: f32,
     pub active_line_background: Color,
     pub highlighted_line_background: Color,
+    pub document_highlight_read_background: Color,
+    pub document_highlight_write_background: Color,
     pub diff_background_deleted: Color,
     pub diff_background_inserted: Color,
     pub line_number: Color,
@@ -386,6 +388,8 @@ impl InputEditorStyle {
             gutter_padding_factor: Default::default(),
             active_line_background: Default::default(),
             highlighted_line_background: Default::default(),
+            document_highlight_read_background: Default::default(),
+            document_highlight_write_background: Default::default(),
             diff_background_deleted: Default::default(),
             diff_background_inserted: Default::default(),
             line_number: Default::default(),

crates/zed/assets/themes/_base.toml 🔗

@@ -249,6 +249,8 @@ gutter_background = "$surface.1"
 gutter_padding_factor = 2.5
 active_line_background = "$state.active_line"
 highlighted_line_background = "$state.highlighted_line"
+document_highlight_read_background = "#99999920"
+document_highlight_write_background = "#99999916"
 diff_background_deleted = "$state.deleted_line"
 diff_background_inserted = "$state.inserted_line"
 line_number = "$text.2.color"