Fix inlay hint hover by not clearing hover when mouse is over inlay
When hovering over an inlay hint, point_for_position.as_valid() returns None because inlays don't have valid text positions. This was causing hover_at(editor, None) to be called, which would hide any active hovers. The fix is simple: don't call hover_at when we're over an inlay position. The inlay hover is already handled by update_hovered_link, so we don't need to do anything else.
This commit is contained in:
parent
01d7b3345b
commit
2ff30d20e3
5 changed files with 141 additions and 706 deletions
|
@ -1061,11 +1061,10 @@ pub struct Editor {
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
leader_id: Option<CollaboratorId>,
|
leader_id: Option<CollaboratorId>,
|
||||||
remote_id: Option<ViewId>,
|
remote_id: Option<ViewId>,
|
||||||
hover_state: HoverState,
|
pub hover_state: HoverState,
|
||||||
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
|
pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
hovered_link_state: Option<HoveredLinkState>,
|
hovered_link_state: Option<HoveredLinkState>,
|
||||||
resolved_inlay_hints_pending_hover: HashSet<InlayId>,
|
|
||||||
edit_prediction_provider: Option<RegisteredInlineCompletionProvider>,
|
edit_prediction_provider: Option<RegisteredInlineCompletionProvider>,
|
||||||
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
||||||
active_inline_completion: Option<InlineCompletionState>,
|
active_inline_completion: Option<InlineCompletionState>,
|
||||||
|
@ -2080,7 +2079,6 @@ impl Editor {
|
||||||
hover_state: HoverState::default(),
|
hover_state: HoverState::default(),
|
||||||
pending_mouse_down: None,
|
pending_mouse_down: None,
|
||||||
hovered_link_state: None,
|
hovered_link_state: None,
|
||||||
resolved_inlay_hints_pending_hover: HashSet::default(),
|
|
||||||
edit_prediction_provider: None,
|
edit_prediction_provider: None,
|
||||||
active_inline_completion: None,
|
active_inline_completion: None,
|
||||||
stale_inline_completion_in_menu: None,
|
stale_inline_completion_in_menu: None,
|
||||||
|
@ -20354,99 +20352,6 @@ impl Editor {
|
||||||
&self.inlay_hint_cache
|
&self.inlay_hint_cache
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_resolved_inlay_hint_hover(
|
|
||||||
&mut self,
|
|
||||||
inlay_id: InlayId,
|
|
||||||
excerpt_id: ExcerptId,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> bool {
|
|
||||||
if !self.resolved_inlay_hints_pending_hover.remove(&inlay_id) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Get the resolved hint from the cache
|
|
||||||
if let Some(cached_hint) = self.inlay_hint_cache.hint_by_id(excerpt_id, inlay_id) {
|
|
||||||
// Check if we have tooltip data to display
|
|
||||||
let mut hover_to_show = None;
|
|
||||||
|
|
||||||
// Check main tooltip
|
|
||||||
if let Some(tooltip) = &cached_hint.tooltip {
|
|
||||||
let inlay_hint = self
|
|
||||||
.visible_inlay_hints(cx)
|
|
||||||
.into_iter()
|
|
||||||
.find(|hint| hint.id == inlay_id);
|
|
||||||
|
|
||||||
if let Some(inlay_hint) = inlay_hint {
|
|
||||||
let range = crate::InlayHighlight {
|
|
||||||
inlay: inlay_id,
|
|
||||||
inlay_position: inlay_hint.position,
|
|
||||||
range: 0..inlay_hint.text.len(),
|
|
||||||
};
|
|
||||||
hover_to_show = Some((tooltip.clone(), range));
|
|
||||||
}
|
|
||||||
} else if let project::InlayHintLabel::LabelParts(parts) = &cached_hint.label {
|
|
||||||
// Check label parts for tooltips
|
|
||||||
let inlay_hint = self
|
|
||||||
.visible_inlay_hints(cx)
|
|
||||||
.into_iter()
|
|
||||||
.find(|hint| hint.id == inlay_id);
|
|
||||||
|
|
||||||
if let Some(inlay_hint) = inlay_hint {
|
|
||||||
let mut offset = 0;
|
|
||||||
for part in parts {
|
|
||||||
if let Some(part_tooltip) = &part.tooltip {
|
|
||||||
let range = crate::InlayHighlight {
|
|
||||||
inlay: inlay_id,
|
|
||||||
inlay_position: inlay_hint.position,
|
|
||||||
range: offset..offset + part.value.len(),
|
|
||||||
};
|
|
||||||
// Convert InlayHintLabelPartTooltip to InlayHintTooltip
|
|
||||||
let tooltip = match part_tooltip {
|
|
||||||
project::InlayHintLabelPartTooltip::String(text) => {
|
|
||||||
project::InlayHintTooltip::String(text.clone())
|
|
||||||
}
|
|
||||||
project::InlayHintLabelPartTooltip::MarkupContent(content) => {
|
|
||||||
project::InlayHintTooltip::MarkupContent(content.clone())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
hover_to_show = Some((tooltip, range));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
offset += part.value.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the hover if we have tooltip data
|
|
||||||
if let Some((tooltip, range)) = hover_to_show {
|
|
||||||
use crate::hover_popover::{InlayHover, hover_at_inlay};
|
|
||||||
use project::{HoverBlock, HoverBlockKind, InlayHintTooltip};
|
|
||||||
|
|
||||||
let hover_block = match tooltip {
|
|
||||||
InlayHintTooltip::String(text) => HoverBlock {
|
|
||||||
text,
|
|
||||||
kind: HoverBlockKind::PlainText,
|
|
||||||
},
|
|
||||||
InlayHintTooltip::MarkupContent(content) => HoverBlock {
|
|
||||||
text: content.value,
|
|
||||||
kind: content.kind,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
hover_at_inlay(
|
|
||||||
self,
|
|
||||||
InlayHover {
|
|
||||||
tooltip: hover_block,
|
|
||||||
range,
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn replay_insert_event(
|
pub fn replay_insert_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
text: &str,
|
text: &str,
|
||||||
|
|
|
@ -121,22 +121,6 @@ impl Editor {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
|
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
|
||||||
|
|
||||||
// Allow inlay hover points to be updated even without modifier key
|
|
||||||
if point_for_position.as_valid().is_none() {
|
|
||||||
// Hovering over inlay - check for hover tooltips
|
|
||||||
update_inlay_link_and_hover_points(
|
|
||||||
snapshot,
|
|
||||||
point_for_position,
|
|
||||||
self,
|
|
||||||
hovered_link_modifier,
|
|
||||||
modifiers.shift,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hovered_link_modifier || self.has_pending_selection() {
|
if !hovered_link_modifier || self.has_pending_selection() {
|
||||||
self.hide_hovered_link(cx);
|
self.hide_hovered_link(cx);
|
||||||
return;
|
return;
|
||||||
|
@ -153,7 +137,15 @@ impl Editor {
|
||||||
show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
|
show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// This case is now handled above
|
update_inlay_link_and_hover_points(
|
||||||
|
snapshot,
|
||||||
|
point_for_position,
|
||||||
|
self,
|
||||||
|
hovered_link_modifier,
|
||||||
|
modifiers.shift,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,164 +319,129 @@ pub fn update_inlay_link_and_hover_points(
|
||||||
let inlay_hint_cache = editor.inlay_hint_cache();
|
let inlay_hint_cache = editor.inlay_hint_cache();
|
||||||
let excerpt_id = previous_valid_anchor.excerpt_id;
|
let excerpt_id = previous_valid_anchor.excerpt_id;
|
||||||
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
|
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
|
||||||
// Check if we should process this hint for hover
|
match cached_hint.resolve_state {
|
||||||
let should_process_hint = match cached_hint.resolve_state {
|
|
||||||
ResolveState::CanResolve(_, _) => {
|
ResolveState::CanResolve(_, _) => {
|
||||||
// Check if the hint already has the data we need (tooltip in label parts)
|
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
|
||||||
if let project::InlayHintLabel::LabelParts(label_parts) = &cached_hint.label
|
inlay_hint_cache.spawn_hint_resolve(
|
||||||
{
|
buffer_id,
|
||||||
let has_tooltip_parts =
|
excerpt_id,
|
||||||
label_parts.iter().any(|part| part.tooltip.is_some());
|
hovered_hint.id,
|
||||||
if has_tooltip_parts {
|
window,
|
||||||
true // Process the hint
|
cx,
|
||||||
} else {
|
);
|
||||||
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
|
|
||||||
inlay_hint_cache.spawn_hint_resolve(
|
|
||||||
buffer_id,
|
|
||||||
excerpt_id,
|
|
||||||
hovered_hint.id,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
false // Don't process further
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Some(buffer_id) = previous_valid_anchor.buffer_id {
|
|
||||||
inlay_hint_cache.spawn_hint_resolve(
|
|
||||||
buffer_id,
|
|
||||||
excerpt_id,
|
|
||||||
hovered_hint.id,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
false // Don't process further
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResolveState::Resolved => {
|
ResolveState::Resolved => {
|
||||||
true // Process the hint
|
let mut extra_shift_left = 0;
|
||||||
}
|
let mut extra_shift_right = 0;
|
||||||
ResolveState::Resolving => {
|
if cached_hint.padding_left {
|
||||||
// Check if this hint was just resolved and needs hover
|
extra_shift_left += 1;
|
||||||
if editor.check_resolved_inlay_hint_hover(
|
extra_shift_right += 1;
|
||||||
hovered_hint.id,
|
|
||||||
excerpt_id,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
return; // Hover was shown by check_resolved_inlay_hint_hover
|
|
||||||
}
|
}
|
||||||
false // Don't process yet
|
if cached_hint.padding_right {
|
||||||
}
|
extra_shift_right += 1;
|
||||||
};
|
|
||||||
|
|
||||||
if should_process_hint {
|
|
||||||
let mut extra_shift_left = 0;
|
|
||||||
let mut extra_shift_right = 0;
|
|
||||||
if cached_hint.padding_left {
|
|
||||||
extra_shift_left += 1;
|
|
||||||
extra_shift_right += 1;
|
|
||||||
}
|
|
||||||
if cached_hint.padding_right {
|
|
||||||
extra_shift_right += 1;
|
|
||||||
}
|
|
||||||
match cached_hint.label {
|
|
||||||
project::InlayHintLabel::String(_) => {
|
|
||||||
if let Some(tooltip) = cached_hint.tooltip {
|
|
||||||
hover_popover::hover_at_inlay(
|
|
||||||
editor,
|
|
||||||
InlayHover {
|
|
||||||
tooltip: match tooltip {
|
|
||||||
InlayHintTooltip::String(text) => HoverBlock {
|
|
||||||
text,
|
|
||||||
kind: HoverBlockKind::PlainText,
|
|
||||||
},
|
|
||||||
InlayHintTooltip::MarkupContent(content) => {
|
|
||||||
HoverBlock {
|
|
||||||
text: content.value,
|
|
||||||
kind: content.kind,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
range: InlayHighlight {
|
|
||||||
inlay: hovered_hint.id,
|
|
||||||
inlay_position: hovered_hint.position,
|
|
||||||
range: extra_shift_left
|
|
||||||
..hovered_hint.text.len() + extra_shift_right,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
hover_updated = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
project::InlayHintLabel::LabelParts(label_parts) => {
|
match cached_hint.label {
|
||||||
let hint_start = snapshot.anchor_to_inlay_offset(hovered_hint.position);
|
project::InlayHintLabel::String(_) => {
|
||||||
if let Some((hovered_hint_part, part_range)) =
|
if let Some(tooltip) = cached_hint.tooltip {
|
||||||
hover_popover::find_hovered_hint_part(
|
|
||||||
label_parts,
|
|
||||||
hint_start,
|
|
||||||
hovered_offset,
|
|
||||||
)
|
|
||||||
{
|
|
||||||
let highlight_start =
|
|
||||||
(part_range.start - hint_start).0 + extra_shift_left;
|
|
||||||
let highlight_end =
|
|
||||||
(part_range.end - hint_start).0 + extra_shift_right;
|
|
||||||
let highlight = InlayHighlight {
|
|
||||||
inlay: hovered_hint.id,
|
|
||||||
inlay_position: hovered_hint.position,
|
|
||||||
range: highlight_start..highlight_end,
|
|
||||||
};
|
|
||||||
if let Some(tooltip) = hovered_hint_part.tooltip {
|
|
||||||
hover_popover::hover_at_inlay(
|
hover_popover::hover_at_inlay(
|
||||||
editor,
|
editor,
|
||||||
InlayHover {
|
InlayHover {
|
||||||
tooltip: match tooltip {
|
tooltip: match tooltip {
|
||||||
InlayHintLabelPartTooltip::String(text) => {
|
InlayHintTooltip::String(text) => HoverBlock {
|
||||||
|
text,
|
||||||
|
kind: HoverBlockKind::PlainText,
|
||||||
|
},
|
||||||
|
InlayHintTooltip::MarkupContent(content) => {
|
||||||
HoverBlock {
|
HoverBlock {
|
||||||
text,
|
text: content.value,
|
||||||
kind: HoverBlockKind::PlainText,
|
kind: content.kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InlayHintLabelPartTooltip::MarkupContent(
|
|
||||||
content,
|
|
||||||
) => HoverBlock {
|
|
||||||
text: content.value,
|
|
||||||
kind: content.kind,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
range: highlight.clone(),
|
range: InlayHighlight {
|
||||||
|
inlay: hovered_hint.id,
|
||||||
|
inlay_position: hovered_hint.position,
|
||||||
|
range: extra_shift_left
|
||||||
|
..hovered_hint.text.len() + extra_shift_right,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
hover_updated = true;
|
hover_updated = true;
|
||||||
}
|
}
|
||||||
if let Some((language_server_id, location)) =
|
}
|
||||||
hovered_hint_part.location
|
project::InlayHintLabel::LabelParts(label_parts) => {
|
||||||
|
let hint_start =
|
||||||
|
snapshot.anchor_to_inlay_offset(hovered_hint.position);
|
||||||
|
if let Some((hovered_hint_part, part_range)) =
|
||||||
|
hover_popover::find_hovered_hint_part(
|
||||||
|
label_parts,
|
||||||
|
hint_start,
|
||||||
|
hovered_offset,
|
||||||
|
)
|
||||||
{
|
{
|
||||||
if secondary_held && !editor.has_pending_nonempty_selection() {
|
let highlight_start =
|
||||||
go_to_definition_updated = true;
|
(part_range.start - hint_start).0 + extra_shift_left;
|
||||||
show_link_definition(
|
let highlight_end =
|
||||||
shift_held,
|
(part_range.end - hint_start).0 + extra_shift_right;
|
||||||
|
let highlight = InlayHighlight {
|
||||||
|
inlay: hovered_hint.id,
|
||||||
|
inlay_position: hovered_hint.position,
|
||||||
|
range: highlight_start..highlight_end,
|
||||||
|
};
|
||||||
|
if let Some(tooltip) = hovered_hint_part.tooltip {
|
||||||
|
hover_popover::hover_at_inlay(
|
||||||
editor,
|
editor,
|
||||||
TriggerPoint::InlayHint(
|
InlayHover {
|
||||||
highlight,
|
tooltip: match tooltip {
|
||||||
location,
|
InlayHintLabelPartTooltip::String(text) => {
|
||||||
language_server_id,
|
HoverBlock {
|
||||||
),
|
text,
|
||||||
snapshot,
|
kind: HoverBlockKind::PlainText,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InlayHintLabelPartTooltip::MarkupContent(
|
||||||
|
content,
|
||||||
|
) => HoverBlock {
|
||||||
|
text: content.value,
|
||||||
|
kind: content.kind,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
range: highlight.clone(),
|
||||||
|
},
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
hover_updated = true;
|
||||||
|
}
|
||||||
|
if let Some((language_server_id, location)) =
|
||||||
|
hovered_hint_part.location
|
||||||
|
{
|
||||||
|
if secondary_held
|
||||||
|
&& !editor.has_pending_nonempty_selection()
|
||||||
|
{
|
||||||
|
go_to_definition_updated = true;
|
||||||
|
show_link_definition(
|
||||||
|
shift_held,
|
||||||
|
editor,
|
||||||
|
TriggerPoint::InlayHint(
|
||||||
|
highlight,
|
||||||
|
location,
|
||||||
|
language_server_id,
|
||||||
|
),
|
||||||
|
snapshot,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
|
ResolveState::Resolving => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,15 +55,7 @@ pub fn hover_at(
|
||||||
if let Some(anchor) = anchor {
|
if let Some(anchor) = anchor {
|
||||||
show_hover(editor, anchor, false, window, cx);
|
show_hover(editor, anchor, false, window, cx);
|
||||||
} else {
|
} else {
|
||||||
// Don't hide hover if there's an active inlay hover
|
hide_hover(editor, cx);
|
||||||
let has_inlay_hover = editor
|
|
||||||
.hover_state
|
|
||||||
.info_popovers
|
|
||||||
.iter()
|
|
||||||
.any(|popover| matches!(popover.symbol_range, RangeInEditor::Inlay(_)));
|
|
||||||
if !has_inlay_hover {
|
|
||||||
hide_hover(editor, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,12 +151,7 @@ pub fn hover_at_inlay(
|
||||||
false
|
false
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
return;
|
hide_hover(editor, cx);
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have an in-progress hover task for a different location
|
|
||||||
if editor.hover_state.info_task.is_some() {
|
|
||||||
editor.hover_state.info_task = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
||||||
|
@ -214,9 +201,7 @@ pub fn hover_at_inlay(
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
.log_err()
|
.log_err()
|
||||||
.await;
|
.await
|
||||||
|
|
||||||
Some(())
|
|
||||||
});
|
});
|
||||||
|
|
||||||
editor.hover_state.info_task = Some(task);
|
editor.hover_state.info_task = Some(task);
|
||||||
|
@ -1898,236 +1883,4 @@ mod tests {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_hover_on_inlay_hint_types(cx: &mut gpui::TestAppContext) {
|
|
||||||
use crate::{DisplayPoint, PointForPosition};
|
|
||||||
|
|
||||||
init_test(cx, |settings| {
|
|
||||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
|
||||||
enabled: true,
|
|
||||||
show_type_hints: true,
|
|
||||||
show_value_hints: true,
|
|
||||||
show_parameter_hints: true,
|
|
||||||
show_other_hints: true,
|
|
||||||
edit_debounce_ms: 0,
|
|
||||||
scroll_debounce_ms: 0,
|
|
||||||
show_background: false,
|
|
||||||
toggle_on_modifiers_press: None,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut cx = EditorLspTestContext::new_rust(
|
|
||||||
lsp::ServerCapabilities {
|
|
||||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
|
||||||
inlay_hint_provider: Some(lsp::OneOf::Right(
|
|
||||||
lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
|
|
||||||
resolve_provider: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
cx.set_state(indoc! {r#"
|
|
||||||
struct String;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let foo = "foo".to_string();
|
|
||||||
let bar: String = "bar".to_string();ˇ
|
|
||||||
}
|
|
||||||
"#});
|
|
||||||
|
|
||||||
// Set up inlay hint handler with proper label parts that include locations
|
|
||||||
let buffer_text = cx.buffer_text();
|
|
||||||
let hint_position = cx.to_lsp(buffer_text.find("foo =").unwrap() + 3);
|
|
||||||
let string_type_range = cx.lsp_range(indoc! {r#"
|
|
||||||
struct «String»;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let foo = "foo".to_string();
|
|
||||||
let bar: String = "bar".to_string();
|
|
||||||
}
|
|
||||||
"#});
|
|
||||||
let uri = cx.buffer_lsp_url.clone();
|
|
||||||
|
|
||||||
cx.set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |_, _params, _| {
|
|
||||||
let uri = uri.clone();
|
|
||||||
async move {
|
|
||||||
Ok(Some(vec![lsp::InlayHint {
|
|
||||||
position: hint_position,
|
|
||||||
label: lsp::InlayHintLabel::LabelParts(vec![
|
|
||||||
lsp::InlayHintLabelPart {
|
|
||||||
value: ": ".to_string(),
|
|
||||||
location: None,
|
|
||||||
tooltip: None,
|
|
||||||
command: None,
|
|
||||||
},
|
|
||||||
lsp::InlayHintLabelPart {
|
|
||||||
value: "String".to_string(),
|
|
||||||
location: Some(lsp::Location {
|
|
||||||
uri: uri.clone(),
|
|
||||||
range: string_type_range,
|
|
||||||
}),
|
|
||||||
tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
|
|
||||||
lsp::MarkupContent {
|
|
||||||
kind: lsp::MarkupKind::Markdown,
|
|
||||||
value: "```rust\nstruct String\n```\n\nA UTF-8 encoded, growable string.".to_string(),
|
|
||||||
}
|
|
||||||
)),
|
|
||||||
command: None,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
kind: Some(lsp::InlayHintKind::TYPE),
|
|
||||||
text_edits: None,
|
|
||||||
tooltip: None,
|
|
||||||
padding_left: Some(false),
|
|
||||||
padding_right: Some(false),
|
|
||||||
data: None,
|
|
||||||
}]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.next()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
|
|
||||||
// Verify inlay hint is displayed
|
|
||||||
cx.update_editor(|editor, _, cx| {
|
|
||||||
let expected_layers = vec![": String".to_string()];
|
|
||||||
assert_eq!(expected_layers, cached_hint_labels(editor));
|
|
||||||
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up hover handler for explicit type hover
|
|
||||||
let mut hover_requests =
|
|
||||||
cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _params, _| {
|
|
||||||
async move {
|
|
||||||
// Return hover info for any hover request (both line 0 and line 4 have String types)
|
|
||||||
Ok(Some(lsp::Hover {
|
|
||||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
|
||||||
kind: lsp::MarkupKind::Markdown,
|
|
||||||
value:
|
|
||||||
"```rust\nstruct String\n```\n\nA UTF-8 encoded, growable string."
|
|
||||||
.to_string(),
|
|
||||||
}),
|
|
||||||
range: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test hovering over the inlay hint type
|
|
||||||
// Get the position where the inlay hint is displayed
|
|
||||||
let inlay_range = cx
|
|
||||||
.ranges(indoc! {r#"
|
|
||||||
struct String;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let foo« »= "foo".to_string();
|
|
||||||
let bar: String = "bar".to_string();
|
|
||||||
}
|
|
||||||
"#})
|
|
||||||
.first()
|
|
||||||
.cloned()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Create a PointForPosition that simulates hovering over the "String" part of the inlay hint
|
|
||||||
let point_for_position = cx.update_editor(|editor, window, cx| {
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
let previous_valid = inlay_range.start.to_display_point(&snapshot);
|
|
||||||
let next_valid = inlay_range.end.to_display_point(&snapshot);
|
|
||||||
// The hint text is ": String", we want to hover over "String" which starts at index 2
|
|
||||||
// Add a bit more to ensure we're well within "String" (e.g., over the 'r')
|
|
||||||
let exact_unclipped = DisplayPoint::new(
|
|
||||||
previous_valid.row(),
|
|
||||||
previous_valid.column() + 4, // Position over 'r' in "String"
|
|
||||||
);
|
|
||||||
PointForPosition {
|
|
||||||
previous_valid,
|
|
||||||
next_valid,
|
|
||||||
exact_unclipped,
|
|
||||||
column_overshoot_after_line_end: 0,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update hovered link to trigger hover logic for inlay hints
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
update_inlay_link_and_hover_points(
|
|
||||||
&snapshot,
|
|
||||||
point_for_position,
|
|
||||||
editor,
|
|
||||||
false, // secondary_held
|
|
||||||
false, // shift_held
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for the popover to appear
|
|
||||||
cx.background_executor
|
|
||||||
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
|
|
||||||
// Check if hover popover is shown for inlay hint
|
|
||||||
let has_inlay_hover = cx.editor(|editor, _, _| editor.hover_state.info_popovers.len() > 0);
|
|
||||||
|
|
||||||
// Clear hover state
|
|
||||||
cx.update_editor(|editor, _, cx| {
|
|
||||||
hide_hover(editor, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test hovering over the explicit type
|
|
||||||
let explicit_string_point = cx.display_point(indoc! {r#"
|
|
||||||
struct String;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let foo = "foo".to_string();
|
|
||||||
let bar: Sˇtring = "bar".to_string();
|
|
||||||
}
|
|
||||||
"#});
|
|
||||||
|
|
||||||
// Use hover_at to trigger hover on explicit type
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
let anchor = snapshot
|
|
||||||
.buffer_snapshot
|
|
||||||
.anchor_before(explicit_string_point.to_offset(&snapshot, Bias::Left));
|
|
||||||
hover_at(editor, Some(anchor), window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for hover request and give time for the popover to appear
|
|
||||||
hover_requests.next().await;
|
|
||||||
cx.background_executor
|
|
||||||
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
|
|
||||||
cx.background_executor.run_until_parked();
|
|
||||||
|
|
||||||
// Check if hover popover is shown for explicit type
|
|
||||||
let has_explicit_hover =
|
|
||||||
cx.editor(|editor, _, _| editor.hover_state.info_popovers.len() > 0);
|
|
||||||
|
|
||||||
// Both should show hover popovers
|
|
||||||
assert!(
|
|
||||||
has_explicit_hover,
|
|
||||||
"Hover popover should be shown when hovering over explicit type"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
has_inlay_hover,
|
|
||||||
"Hover popover should be shown when hovering over inlay hint type"
|
|
||||||
);
|
|
||||||
|
|
||||||
// NOTE: This test demonstrates the proper fix for issue #33715:
|
|
||||||
// Language servers should provide inlay hints with label parts that include
|
|
||||||
// tooltip information. When users hover over a type in an inlay hint,
|
|
||||||
// they see the tooltip content, which should contain the same information
|
|
||||||
// as hovering over an actual type annotation.
|
|
||||||
//
|
|
||||||
// The issue occurs when language servers provide inlay hints as plain strings
|
|
||||||
// (e.g., ": String") without any tooltip information. In that case, there's
|
|
||||||
// no way for Zed to know what documentation to show when hovering.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ use clock::Global;
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
|
use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
|
||||||
use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind};
|
use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind};
|
||||||
use lsp::LanguageServerId;
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use project::{InlayHint, ResolveState};
|
use project::{InlayHint, ResolveState};
|
||||||
|
|
||||||
|
@ -623,67 +622,45 @@ impl InlayHintCache {
|
||||||
let mut guard = excerpt_hints.write();
|
let mut guard = excerpt_hints.write();
|
||||||
if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
|
if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
|
||||||
if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
|
if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
|
||||||
let server_id = *server_id;
|
|
||||||
let hint_to_resolve = cached_hint.clone();
|
let hint_to_resolve = cached_hint.clone();
|
||||||
|
let server_id = *server_id;
|
||||||
cached_hint.resolve_state = ResolveState::Resolving;
|
cached_hint.resolve_state = ResolveState::Resolving;
|
||||||
drop(guard);
|
drop(guard);
|
||||||
self.resolve_hint(
|
cx.spawn_in(window, async move |editor, cx| {
|
||||||
server_id,
|
let resolved_hint_task = editor.update(cx, |editor, cx| {
|
||||||
buffer_id,
|
let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
|
||||||
excerpt_id,
|
editor.semantics_provider.as_ref()?.resolve_inlay_hint(
|
||||||
id,
|
hint_to_resolve,
|
||||||
hint_to_resolve,
|
buffer,
|
||||||
window,
|
server_id,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
})?;
|
||||||
|
if let Some(resolved_hint_task) = resolved_hint_task {
|
||||||
|
let mut resolved_hint =
|
||||||
|
resolved_hint_task.await.context("hint resolve task")?;
|
||||||
|
editor.read_with(cx, |editor, _| {
|
||||||
|
if let Some(excerpt_hints) =
|
||||||
|
editor.inlay_hint_cache.hints.get(&excerpt_id)
|
||||||
|
{
|
||||||
|
let mut guard = excerpt_hints.write();
|
||||||
|
if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
|
||||||
|
if cached_hint.resolve_state == ResolveState::Resolving {
|
||||||
|
resolved_hint.resolve_state = ResolveState::Resolved;
|
||||||
|
*cached_hint = resolved_hint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_hint(
|
|
||||||
&self,
|
|
||||||
server_id: LanguageServerId,
|
|
||||||
buffer_id: BufferId,
|
|
||||||
excerpt_id: ExcerptId,
|
|
||||||
inlay_id: InlayId,
|
|
||||||
hint_to_resolve: InlayHint,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Editor>,
|
|
||||||
) -> Task<anyhow::Result<()>> {
|
|
||||||
cx.spawn_in(window, async move |editor, cx| {
|
|
||||||
let resolved_hint_task = editor.update(cx, |editor, cx| {
|
|
||||||
let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
|
|
||||||
editor.semantics_provider.as_ref()?.resolve_inlay_hint(
|
|
||||||
hint_to_resolve,
|
|
||||||
buffer,
|
|
||||||
server_id,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if let Some(resolved_hint_task) = resolved_hint_task {
|
|
||||||
let mut resolved_hint = resolved_hint_task.await.context("hint resolve task")?;
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
if let Some(excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
|
|
||||||
let mut guard = excerpt_hints.write();
|
|
||||||
if let Some(cached_hint) = guard.hints_by_id.get_mut(&inlay_id) {
|
|
||||||
if cached_hint.resolve_state == ResolveState::Resolving {
|
|
||||||
resolved_hint.resolve_state = ResolveState::Resolved;
|
|
||||||
*cached_hint = resolved_hint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the hint as resolved and needing hover check
|
|
||||||
editor.resolved_inlay_hints_pending_hover.insert(inlay_id);
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
|
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
|
||||||
|
|
|
@ -4965,10 +4965,7 @@ impl LspStore {
|
||||||
return Task::ready(Ok(hint));
|
return Task::ready(Ok(hint));
|
||||||
}
|
}
|
||||||
let buffer_snapshot = buffer_handle.read(cx).snapshot();
|
let buffer_snapshot = buffer_handle.read(cx).snapshot();
|
||||||
// Preserve the original hint data before resolution
|
cx.spawn(async move |_, cx| {
|
||||||
let original_hint = hint.clone();
|
|
||||||
|
|
||||||
cx.spawn(async move |_this, cx| {
|
|
||||||
let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
|
let resolve_task = lang_server.request::<lsp::request::InlayHintResolveRequest>(
|
||||||
InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
|
InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
|
||||||
);
|
);
|
||||||
|
@ -4976,139 +4973,7 @@ impl LspStore {
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.context("inlay hint resolve LSP request")?;
|
.context("inlay hint resolve LSP request")?;
|
||||||
|
let resolved_hint = InlayHints::lsp_to_project_hint(
|
||||||
// Check if we need to fetch hover info as a fallback
|
|
||||||
let needs_fallback = match &resolved_hint.label {
|
|
||||||
lsp::InlayHintLabel::String(_) => resolved_hint.tooltip.is_none(),
|
|
||||||
lsp::InlayHintLabel::LabelParts(parts) => {
|
|
||||||
resolved_hint.tooltip.is_none()
|
|
||||||
&& parts
|
|
||||||
.iter()
|
|
||||||
.any(|p| p.tooltip.is_none() && p.location.is_some())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut resolved_hint = resolved_hint;
|
|
||||||
|
|
||||||
if let lsp::InlayHintLabel::LabelParts(parts) = &resolved_hint.label {
|
|
||||||
for (i, part) in parts.iter().enumerate() {
|
|
||||||
" Part {}: value='{}', tooltip={:?}, location={:?}",
|
|
||||||
i, part.value, part.tooltip, part.location
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if needs_fallback {
|
|
||||||
// For label parts with locations but no tooltips, fetch hover info
|
|
||||||
if let lsp::InlayHintLabel::LabelParts(parts) = &mut resolved_hint.label {
|
|
||||||
for part in parts.iter_mut() {
|
|
||||||
if part.tooltip.is_none() {
|
|
||||||
if let Some(location) = &part.location {
|
|
||||||
|
|
||||||
// Open the document
|
|
||||||
let did_open_params = lsp::DidOpenTextDocumentParams {
|
|
||||||
text_document: lsp::TextDocumentItem {
|
|
||||||
uri: location.uri.clone(),
|
|
||||||
language_id: "rust".to_string(), // TODO: Detect language
|
|
||||||
version: 0,
|
|
||||||
text: std::fs::read_to_string(location.uri.path())
|
|
||||||
.unwrap_or_else(|_| String::new()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
lang_server.notify::<lsp::notification::DidOpenTextDocument>(
|
|
||||||
&did_open_params,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Request hover at the location
|
|
||||||
let hover_params = lsp::HoverParams {
|
|
||||||
text_document_position_params:
|
|
||||||
lsp::TextDocumentPositionParams {
|
|
||||||
text_document: lsp::TextDocumentIdentifier {
|
|
||||||
uri: location.uri.clone(),
|
|
||||||
},
|
|
||||||
position: location.range.start,
|
|
||||||
},
|
|
||||||
work_done_progress_params: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(hover_response) = lang_server
|
|
||||||
.request::<lsp::request::HoverRequest>(hover_params)
|
|
||||||
.await
|
|
||||||
.into_response()
|
|
||||||
{
|
|
||||||
|
|
||||||
if let Some(hover) = hover_response {
|
|
||||||
// Convert hover contents to tooltip
|
|
||||||
part.tooltip = Some(match hover.contents {
|
|
||||||
lsp::HoverContents::Scalar(content) => {
|
|
||||||
lsp::InlayHintLabelPartTooltip::String(
|
|
||||||
match content {
|
|
||||||
lsp::MarkedString::String(s) => s,
|
|
||||||
lsp::MarkedString::LanguageString(
|
|
||||||
ls,
|
|
||||||
) => ls.value,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
lsp::HoverContents::Array(contents) => {
|
|
||||||
let combined = contents
|
|
||||||
.into_iter()
|
|
||||||
.map(|c| match c {
|
|
||||||
lsp::MarkedString::String(s) => s,
|
|
||||||
lsp::MarkedString::LanguageString(
|
|
||||||
ls,
|
|
||||||
) => ls.value,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n\n");
|
|
||||||
lsp::InlayHintLabelPartTooltip::String(combined)
|
|
||||||
}
|
|
||||||
lsp::HoverContents::Markup(markup) => {
|
|
||||||
lsp::InlayHintLabelPartTooltip::MarkupContent(
|
|
||||||
markup,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the document
|
|
||||||
let did_close_params = lsp::DidCloseTextDocumentParams {
|
|
||||||
text_document: lsp::TextDocumentIdentifier {
|
|
||||||
uri: location.uri.clone(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
lang_server.notify::<lsp::notification::DidCloseTextDocument>(
|
|
||||||
&did_close_params,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we need to restore location data from the original hint
|
|
||||||
let mut resolved_hint = resolved_hint;
|
|
||||||
if let (
|
|
||||||
lsp::InlayHintLabel::LabelParts(resolved_parts),
|
|
||||||
crate::InlayHintLabel::LabelParts(original_parts),
|
|
||||||
) = (&mut resolved_hint.label, &original_hint.label)
|
|
||||||
{
|
|
||||||
for (resolved_part, original_part) in
|
|
||||||
resolved_parts.iter_mut().zip(original_parts.iter())
|
|
||||||
{
|
|
||||||
if resolved_part.location.is_none() && original_part.location.is_some() {
|
|
||||||
// Restore location from original hint
|
|
||||||
if let Some((_server_id, location)) = &original_part.location {
|
|
||||||
resolved_part.location = Some(location.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut resolved_hint = InlayHints::lsp_to_project_hint(
|
|
||||||
resolved_hint,
|
resolved_hint,
|
||||||
&buffer_handle,
|
&buffer_handle,
|
||||||
server_id,
|
server_id,
|
||||||
|
@ -5117,28 +4982,6 @@ impl LspStore {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Final check: if resolved hint still has no tooltip but original had location,
|
|
||||||
// preserve the original hint's data
|
|
||||||
if resolved_hint.tooltip.is_none() {
|
|
||||||
if let (
|
|
||||||
crate::InlayHintLabel::LabelParts(resolved_parts),
|
|
||||||
crate::InlayHintLabel::LabelParts(original_parts),
|
|
||||||
) = (&mut resolved_hint.label, &original_hint.label)
|
|
||||||
{
|
|
||||||
for (resolved_part, original_part) in
|
|
||||||
resolved_parts.iter_mut().zip(original_parts.iter())
|
|
||||||
{
|
|
||||||
if resolved_part.tooltip.is_none()
|
|
||||||
&& resolved_part.location.is_none()
|
|
||||||
&& original_part.location.is_some()
|
|
||||||
{
|
|
||||||
resolved_part.location = original_part.location.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(resolved_hint)
|
Ok(resolved_hint)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue