Prepare editor to display multiple LSP hover responses for the same place (#9868)

This commit is contained in:
Kirill Bulatov 2024-03-27 20:49:26 +01:00 committed by GitHub
parent ce37885f49
commit 80242584e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 163 additions and 106 deletions

View File

@ -4978,11 +4978,16 @@ async fn test_lsp_hover(
},
);
let hover_info = project_b
let hovers = project_b
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
.await
.unwrap()
.unwrap();
assert_eq!(
hovers.len(),
1,
"Expected exactly one hover but got: {hovers:?}"
);
let hover_info = hovers.into_iter().next().unwrap();
buffer_b.read_with(cx_b, |buffer, _| {
let snapshot = buffer.snapshot();

View File

@ -4,17 +4,18 @@ use crate::{
Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
ExcerptId, Hover, RangeToAnchorExt,
};
use futures::FutureExt;
use futures::{stream::FuturesUnordered, FutureExt};
use gpui::{
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, Model, MouseButton,
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
ViewContext, WeakView,
};
use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
use lsp::DiagnosticSeverity;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
use smol::stream::StreamExt;
use std::{ops::Range, sync::Arc, time::Duration};
use ui::{prelude::*, Tooltip};
use util::TryFutureExt;
@ -83,13 +84,20 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
return;
};
if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
if let RangeInEditor::Inlay(range) = symbol_range {
if range == &inlay_hover.range {
// Hover triggered from same location as last time. Don't show again.
return;
if editor
.hover_state
.info_popovers
.iter()
.any(|InfoPopover { symbol_range, .. }| {
if let RangeInEditor::Inlay(range) = symbol_range {
if range == &inlay_hover.range {
// Hover triggered from same location as last time. Don't show again.
return true;
}
}
}
false
})
{
hide_hover(editor, cx);
}
@ -107,15 +115,13 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
let hover_popover = InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
blocks,
parsed_content,
};
this.update(&mut cx, |this, cx| {
// TODO: no background highlights happen for inlays currently
this.hover_state.info_popover = Some(hover_popover);
this.hover_state.info_popovers = vec![hover_popover];
cx.notify();
})?;
@ -132,8 +138,9 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
/// selections changed.
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
let did_hide = editor.hover_state.info_popover.take().is_some()
| editor.hover_state.diagnostic_popover.take().is_some();
let info_popovers = editor.hover_state.info_popovers.drain(..);
let diagnostics_popover = editor.hover_state.diagnostic_popover.take();
let did_hide = info_popovers.count() > 0 || diagnostics_popover.is_some();
editor.hover_state.info_task = None;
editor.hover_state.triggered_from = None;
@ -190,22 +197,26 @@ fn show_hover(
};
if !ignore_timeout {
if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
if symbol_range
.as_text_range()
.map(|range| {
let hover_range = range.to_offset(&snapshot.buffer_snapshot);
// LSP returns a hover result for the end index of ranges that should be hovered, so we need to
// use an inclusive range here to check if we should dismiss the popover
(hover_range.start..=hover_range.end).contains(&multibuffer_offset)
})
.unwrap_or(false)
{
// Hover triggered from same location as last time. Don't show again.
return;
} else {
hide_hover(editor, cx);
}
if editor
.hover_state
.info_popovers
.iter()
.any(|InfoPopover { symbol_range, .. }| {
symbol_range
.as_text_range()
.map(|range| {
let hover_range = range.to_offset(&snapshot.buffer_snapshot);
// LSP returns a hover result for the end index of ranges that should be hovered, so we need to
// use an inclusive range here to check if we should dismiss the popover
(hover_range.start..=hover_range.end).contains(&multibuffer_offset)
})
.unwrap_or(false)
})
{
// Hover triggered from same location as last time. Don't show again.
return;
} else {
hide_hover(editor, cx);
}
}
@ -284,10 +295,14 @@ fn show_hover(
});
})?;
let hover_result = hover_request.await.ok().flatten();
let hovers_response = hover_request.await.ok().unwrap_or_default();
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
let hover_popover = match hover_result {
Some(hover_result) if !hover_result.is_empty() => {
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
let mut info_popovers = Vec::with_capacity(hovers_response.len());
let mut info_popover_tasks = hovers_response
.into_iter()
.map(|hover_result| async {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = hover_result
.range
@ -303,44 +318,42 @@ fn show_hover(
})
.unwrap_or_else(|| anchor..anchor);
let language_registry =
project.update(&mut cx, |p, _| p.languages().clone())?;
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
Some(InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Text(range),
blocks,
parsed_content,
})
}
(
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
},
)
})
.collect::<FuturesUnordered<_>>();
while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
hover_highlights.push(highlight_range);
info_popovers.push(info_popover);
}
_ => None,
};
this.update(&mut cx, |this, cx| {
if let Some(symbol_range) = hover_popover
.as_ref()
.and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
{
this.update(&mut cx, |editor, cx| {
if hover_highlights.is_empty() {
editor.clear_background_highlights::<HoverState>(cx);
} else {
// Highlight the selected symbol using a background highlight
this.highlight_background::<HoverState>(
vec![symbol_range],
editor.highlight_background::<HoverState>(
hover_highlights,
|theme| theme.element_hover, // todo update theme
cx,
);
} else {
this.clear_background_highlights::<HoverState>(cx);
}
this.hover_state.info_popover = hover_popover;
editor.hover_state.info_popovers = info_popovers;
cx.notify();
cx.refresh();
})?;
Ok::<_, anyhow::Error>(())
anyhow::Ok(())
}
.log_err()
});
@ -422,7 +435,7 @@ async fn parse_blocks(
#[derive(Default)]
pub struct HoverState {
pub info_popover: Option<InfoPopover>,
pub info_popovers: Vec<InfoPopover>,
pub diagnostic_popover: Option<DiagnosticPopover>,
pub triggered_from: Option<Anchor>,
pub info_task: Option<Task<Option<()>>>,
@ -430,7 +443,7 @@ pub struct HoverState {
impl HoverState {
pub fn visible(&self) -> bool {
self.info_popover.is_some() || self.diagnostic_popover.is_some()
!self.info_popovers.is_empty() || self.diagnostic_popover.is_some()
}
pub fn render(
@ -449,12 +462,20 @@ impl HoverState {
.as_ref()
.map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
.or_else(|| {
self.info_popover
.as_ref()
.map(|info_popover| match &info_popover.symbol_range {
RangeInEditor::Text(range) => &range.start,
RangeInEditor::Inlay(range) => &range.inlay_position,
})
self.info_popovers.iter().find_map(|info_popover| {
match &info_popover.symbol_range {
RangeInEditor::Text(range) => Some(&range.start),
RangeInEditor::Inlay(_) => None,
}
})
})
.or_else(|| {
self.info_popovers.iter().find_map(|info_popover| {
match &info_popover.symbol_range {
RangeInEditor::Text(_) => None,
RangeInEditor::Inlay(range) => Some(&range.inlay_position),
}
})
})?;
let point = anchor.to_display_point(&snapshot.display_snapshot);
@ -468,8 +489,8 @@ impl HoverState {
if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
elements.push(diagnostic_popover.render(style, max_size, cx));
}
if let Some(info_popover) = self.info_popover.as_mut() {
elements.push(info_popover.render(style, max_size, workspace, cx));
for info_popover in &mut self.info_popovers {
elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
}
Some((point, elements))
@ -478,9 +499,7 @@ impl HoverState {
#[derive(Debug, Clone)]
pub struct InfoPopover {
pub project: Model<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
parsed_content: ParsedMarkdown,
}
@ -664,12 +683,19 @@ mod tests {
cx.editor(|editor, _| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popover.clone().unwrap().blocks,
vec![HoverBlock {
text: "some basic docs".to_string(),
kind: HoverBlockKind::Markdown,
},]
)
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
});
// Mouse moved with no hover response dismisses
@ -724,12 +750,19 @@ mod tests {
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
assert_eq!(
editor.hover_state.info_popover.clone().unwrap().blocks,
vec![HoverBlock {
text: "some other basic docs".to_string(),
kind: HoverBlockKind::Markdown,
}]
)
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some other basic docs".to_string())
});
}
@ -773,11 +806,21 @@ mod tests {
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
assert_eq!(
editor.hover_state.info_popover.clone().unwrap().blocks,
vec![HoverBlock {
text: "regular text for hover to show".to_string(),
kind: HoverBlockKind::Markdown,
}],
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(
rendered.text,
"regular text for hover to show".to_string(),
"No empty string hovers should be shown"
);
});
@ -824,20 +867,21 @@ mod tests {
.next()
.await;
let languages = cx.language_registry().clone();
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
assert_eq!(
blocks,
vec![HoverBlock {
text: markdown_string,
kind: HoverBlockKind::Markdown,
}],
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
let rendered = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(
rendered.text,
code_str.trim(),
@ -889,7 +933,9 @@ mod tests {
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _| {
assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
assert!(
hover_state.diagnostic_popover.is_some() && hover_state.info_popovers.is_empty()
)
});
// Info Popover shows after request responded to
@ -1289,8 +1335,10 @@ mod tests {
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state;
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
let popover = hover_state.info_popover.as_ref().unwrap();
assert!(
hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
);
let popover = hover_state.info_popovers.first().cloned().unwrap();
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
assert_eq!(
popover.symbol_range,
@ -1342,8 +1390,10 @@ mod tests {
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state;
assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
let popover = hover_state.info_popover.as_ref().unwrap();
assert!(
hover_state.diagnostic_popover.is_none() && hover_state.info_popovers.len() == 1
);
let popover = hover_state.info_popovers.first().cloned().unwrap();
let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
assert_eq!(
popover.symbol_range,

View File

@ -5189,20 +5189,22 @@ impl Project {
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Hover>>> {
self.request_lsp(
) -> Task<Result<Vec<Hover>>> {
let request_task = self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetHover { position },
cx,
)
);
cx.spawn(|_, _| async move { request_task.await.map(|hover| hover.into_iter().collect()) })
}
pub fn hover<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Hover>>> {
) -> Task<Result<Vec<Hover>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.hover_impl(buffer, position, cx)
}