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)) .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
.await .await
.unwrap()
.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, _| { buffer_b.read_with(cx_b, |buffer, _| {
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();

View file

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

View file

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