Store inlay hint resolve data

Kirill Bulatov created

Change summary

crates/editor/src/display_map/inlay_map.rs |   6 
crates/project/src/lsp_command.rs          | 149 +++++++++++++++--------
crates/project/src/project.rs              |  16 ++
crates/rpc/proto/zed.proto                 |  16 ++
4 files changed, 134 insertions(+), 53 deletions(-)

Detailed changes

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

@@ -1109,7 +1109,7 @@ mod tests {
     use super::*;
     use crate::{InlayId, MultiBuffer};
     use gpui::AppContext;
-    use project::{InlayHint, InlayHintLabel};
+    use project::{InlayHint, InlayHintLabel, ResolveState};
     use rand::prelude::*;
     use settings::SettingsStore;
     use std::{cmp::Reverse, env, sync::Arc};
@@ -1131,6 +1131,7 @@ mod tests {
                     padding_right: false,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1151,6 +1152,7 @@ mod tests {
                     padding_right: true,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1171,6 +1173,7 @@ mod tests {
                     padding_right: false,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text
@@ -1191,6 +1194,7 @@ mod tests {
                     padding_right: true,
                     tooltip: None,
                     kind: None,
+                    resolve_state: ResolveState::Resolved,
                 },
             )
             .text

crates/project/src/lsp_command.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
     InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
-    MarkupContent, Project, ProjectTransaction,
+    MarkupContent, Project, ProjectTransaction, ResolveState,
 };
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
@@ -1817,7 +1817,8 @@ impl LspCommand for InlayHints {
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
     ) -> Result<Vec<InlayHint>> {
-        let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+        let (lsp_adapter, lsp_server) =
+            language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
         // `typescript-language-server` adds padding to the left for type hints, turning
         // `const foo: boolean` into `const foo : boolean` which looks odd.
         // `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
@@ -1833,6 +1834,15 @@ impl LspCommand for InlayHints {
                 .unwrap_or_default()
                 .into_iter()
                 .map(|lsp_hint| {
+                    let resolve_state = match lsp_server.capabilities().inlay_hint_provider {
+                        Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options(
+                            lsp::InlayHintOptions {
+                                resolve_provider: Some(true),
+                                ..
+                            },
+                        ))) => ResolveState::CanResolve(lsp_hint.data),
+                        _ => ResolveState::Resolved,
+                    };
                     let kind = lsp_hint.kind.and_then(|kind| match kind {
                         lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
                         lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
@@ -1910,6 +1920,7 @@ impl LspCommand for InlayHints {
                                 })
                             }
                         }),
+                        resolve_state,
                     }
                 })
                 .collect())
@@ -1959,57 +1970,69 @@ impl LspCommand for InlayHints {
         proto::InlayHintsResponse {
             hints: response
                 .into_iter()
-                .map(|response_hint| proto::InlayHint {
-                    position: Some(language::proto::serialize_anchor(&response_hint.position)),
-                    padding_left: response_hint.padding_left,
-                    padding_right: response_hint.padding_right,
-                    label: Some(proto::InlayHintLabel {
-                        label: Some(match response_hint.label {
-                            InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
-                            InlayHintLabel::LabelParts(label_parts) => {
-                                proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
-                                    parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
-                                        value: label_part.value,
-                                        tooltip: label_part.tooltip.map(|tooltip| {
-                                            let proto_tooltip = match tooltip {
-                                                InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
-                                                InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
-                                                    kind: markup_content.kind,
-                                                    value: markup_content.value,
-                                                }),
-                                            };
-                                            proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
-                                        }),
-                                        location: label_part.location.map(|location| proto::Location {
-                                            start: Some(serialize_anchor(&location.range.start)),
-                                            end: Some(serialize_anchor(&location.range.end)),
-                                            buffer_id: location.buffer.read(cx).remote_id(),
-                                        }),
-                                    }).collect()
-                                })
-                            }
+                .map(|response_hint| {
+                    let (state, lsp_resolve_state) = match response_hint.resolve_state {
+                        ResolveState::CanResolve(resolve_data) => {
+                            (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value }))
+                        }
+                        ResolveState::Resolved => (1, None),
+                        ResolveState::Resolving => (2, None),
+                    };
+                    let resolve_state = Some(proto::ResolveState {
+                        state, lsp_resolve_state
+                    });
+                    proto::InlayHint {
+                        position: Some(language::proto::serialize_anchor(&response_hint.position)),
+                        padding_left: response_hint.padding_left,
+                        padding_right: response_hint.padding_right,
+                        label: Some(proto::InlayHintLabel {
+                            label: Some(match response_hint.label {
+                                InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
+                                InlayHintLabel::LabelParts(label_parts) => {
+                                    proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
+                                        parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
+                                            value: label_part.value,
+                                            tooltip: label_part.tooltip.map(|tooltip| {
+                                                let proto_tooltip = match tooltip {
+                                                    InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
+                                                    InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
+                                                        kind: markup_content.kind,
+                                                        value: markup_content.value,
+                                                    }),
+                                                };
+                                                proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
+                                            }),
+                                            location: label_part.location.map(|location| proto::Location {
+                                                start: Some(serialize_anchor(&location.range.start)),
+                                                end: Some(serialize_anchor(&location.range.end)),
+                                                buffer_id: location.buffer.read(cx).remote_id(),
+                                            }),
+                                        }).collect()
+                                    })
+                                }
+                            }),
                         }),
-                    }),
-                    kind: response_hint.kind.map(|kind| kind.name().to_string()),
-                    tooltip: response_hint.tooltip.map(|response_tooltip| {
-                        let proto_tooltip = match response_tooltip {
-                            InlayHintTooltip::String(s) => {
-                                proto::inlay_hint_tooltip::Content::Value(s)
-                            }
-                            InlayHintTooltip::MarkupContent(markup_content) => {
-                                proto::inlay_hint_tooltip::Content::MarkupContent(
-                                    proto::MarkupContent {
-                                        kind: markup_content.kind,
-                                        value: markup_content.value,
-                                    },
-                                )
+                        kind: response_hint.kind.map(|kind| kind.name().to_string()),
+                        tooltip: response_hint.tooltip.map(|response_tooltip| {
+                            let proto_tooltip = match response_tooltip {
+                                InlayHintTooltip::String(s) => {
+                                    proto::inlay_hint_tooltip::Content::Value(s)
+                                }
+                                InlayHintTooltip::MarkupContent(markup_content) => {
+                                    proto::inlay_hint_tooltip::Content::MarkupContent(
+                                        proto::MarkupContent {
+                                            kind: markup_content.kind,
+                                            value: markup_content.value,
+                                        },
+                                    )
+                                }
+                            };
+                            proto::InlayHintTooltip {
+                                content: Some(proto_tooltip),
                             }
-                        };
-                        proto::InlayHintTooltip {
-                            content: Some(proto_tooltip),
-                        }
-                    }),
-                })
+                        }),
+                        resolve_state,
+                    }})
                 .collect(),
             version: serialize_version(buffer_version),
         }
@@ -2021,7 +2044,7 @@ impl LspCommand for InlayHints {
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
         mut cx: AsyncAppContext,
-    ) -> Result<Vec<InlayHint>> {
+    ) -> anyhow::Result<Vec<InlayHint>> {
         buffer
             .update(&mut cx, |buffer, _| {
                 buffer.wait_for_version(deserialize_version(&message.version))
@@ -2035,6 +2058,27 @@ impl LspCommand for InlayHints {
                 .as_ref()
                 .and_then(|location| location.buffer_id)
                 .context("missing buffer id")?;
+            let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| {
+                panic!(
+                    "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",
+                )
+            });
+
+            let lsp_resolve_state = resolve_state
+                .lsp_resolve_state.as_ref()
+                .map(|lsp_resolve_state| {
+                    serde_json::from_str::<lsp::LSPAny>(&lsp_resolve_state.value)
+                        .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}"))
+                })
+                .transpose()?;
+            let resolve_state = match resolve_state.state {
+                0 => ResolveState::Resolved,
+                1 => ResolveState::CanResolve(lsp_resolve_state),
+                2 => ResolveState::Resolving,
+                invalid => {
+                    anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}")
+                }
+            };
             let hint = InlayHint {
                 buffer_id,
                 position: message_hint
@@ -2103,6 +2147,7 @@ impl LspCommand for InlayHints {
                         }
                     })
                 }),
+                resolve_state,
             };
 
             hints.push(hint);

crates/project/src/project.rs 🔗

@@ -342,6 +342,22 @@ pub struct InlayHint {
     pub padding_left: bool,
     pub padding_right: bool,
     pub tooltip: Option<InlayHintTooltip>,
+    pub resolve_state: ResolveState,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ResolveState {
+    Resolved,
+    CanResolve(Option<lsp::LSPAny>),
+    Resolving,
+}
+
+impl Hash for ResolveState {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        // Regular `lsp::LSPAny` is not hashable, so we can't hash it.
+        // LSP expects this data to not to change between requests, so we only hash the discriminant.
+        std::mem::discriminant(self).hash(state);
+    }
 }
 
 impl InlayHint {

crates/rpc/proto/zed.proto 🔗

@@ -754,6 +754,7 @@ message InlayHint {
     bool padding_left = 4;
     bool padding_right = 5;
     InlayHintTooltip tooltip = 6;
+    ResolveState resolve_state = 7;
 }
 
 message InlayHintLabel {
@@ -787,6 +788,21 @@ message InlayHintLabelPartTooltip {
     }
 }
 
+message ResolveState {
+    State state = 1;
+    LspResolveState lsp_resolve_state = 2;
+
+    enum State {
+        Resolved = 0;
+        CanResolve = 1;
+        Resolving = 2;
+    }
+
+    message LspResolveState {
+        string value = 1;
+    }
+}
+
 message RefreshInlayHints {
     uint64 project_id = 1;
 }