use crate::{ display_map::{InlayOffset, ToDisplayPoint}, hover_links::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, Hover, RangeToAnchorExt,};use futures::{stream::FuturesUnordered, FutureExt};use gpui::{ div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task, ViewContext, WeakView,};use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; use lsp::DiagnosticSeverity;use multi_buffer::ToOffset;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;use workspace::Workspace; pub const HOVER_DELAY_MILLIS: u64 = 350;pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);pub const HOVER_POPOVER_GAP: Pixels = px(10.); /// Bindable action which uses the most recent selection head to trigger a hoverpub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) { let head = editor.selections.newest_anchor().head(); show_hover(editor, head, true, cx);} /// The internal hover action dispatches between `show_hover` or `hide_hover`/// depending on whether a point to hover over is provided.pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) { if EditorSettings::get_global(cx).hover_popover_enabled { if let Some(anchor) = anchor { show_hover(editor, anchor, false, cx); } else { hide_hover(editor, cx); } }} pub struct InlayHover { pub excerpt: ExcerptId, pub range: InlayHighlight, pub tooltip: HoverBlock,} pub fn find_hovered_hint_part( label_parts: Vec<InlayHintLabelPart>, hint_start: InlayOffset, hovered_offset: InlayOffset,) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> { if hovered_offset >= hint_start { let mut hovered_character = (hovered_offset - hint_start).0; let mut part_start = hint_start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { hovered_character -= part_len; part_start.0 += part_len; } else { let part_end = InlayOffset(part_start.0 + part_len); return Some((part, part_start..part_end)); } } } None} pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) { if EditorSettings::get_global(cx).hover_popover_enabled { if editor.pending_rename.is_some() { return; } let Some(project) = editor.project.clone() else { 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); } let task = cx.spawn(|this, mut cx| { async move { cx.background_executor() .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) .await; this.update(&mut cx, |this, _| { this.hover_state.diagnostic_popover = None; })?; let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; let blocks = vec![inlay_hover.tooltip]; let parsed_content = parse_blocks(&blocks, &language_registry, None).await; let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, }; this.update(&mut cx, |this, cx| { // TODO: no background highlights happen for inlays currently this.hover_state.info_popovers = vec![hover_popover]; cx.notify(); })?; anyhow::Ok(()) } .log_err() }); editor.hover_state.info_task = Some(task); }} /// Hides the type information popup./// 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 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; editor.clear_background_highlights::<HoverState>(cx); if did_hide { cx.notify(); } did_hide} /// Queries the LSP and shows type info and documentation/// about the symbol the mouse is currently hovering over./// Triggered by the `Hover` action when the cursor may be over a symbol.fn show_hover( editor: &mut Editor, anchor: Anchor, ignore_timeout: bool, cx: &mut ViewContext<Editor>,) { if editor.pending_rename.is_some() { return; } let snapshot = editor.snapshot(cx); let (buffer, buffer_position) = if let Some(output) = editor.buffer.read(cx).text_anchor_for_position(anchor, cx) { output } else { return; }; let excerpt_id = if let Some((excerpt_id, _, _)) = editor.buffer().read(cx).excerpt_containing(anchor, cx) { excerpt_id } else { return; }; let project = if let Some(project) = editor.project.clone() { project } else { return; }; if !ignore_timeout { 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); let offset = anchor.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(&offset) }) .unwrap_or(false) }) { // Hover triggered from same location as last time. Don't show again. return; } else { hide_hover(editor, cx); } } // Don't request again if the location is the same as the previous request if let Some(triggered_from) = &editor.hover_state.triggered_from { if triggered_from .cmp(&anchor, &snapshot.buffer_snapshot) .is_eq() { return; } } let task = cx.spawn(|this, mut cx| { async move { // If we need to delay, delay a set amount initially before making the lsp request let delay = if ignore_timeout { None } else { // Construct delay task to wait for later let total_delay = Some( cx.background_executor() .timer(Duration::from_millis(HOVER_DELAY_MILLIS)), ); cx.background_executor() .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS)) .await; total_delay }; // query the LSP for hover info let hover_request = cx.update(|cx| { project.update(cx, |project, cx| { project.hover(&buffer, buffer_position, cx) }) })?; if let Some(delay) = delay { delay.await; } // If there's a diagnostic, assign it on the hover state and notify let local_diagnostic = snapshot .buffer_snapshot .diagnostics_in_range::<_, usize>(anchor..anchor, false) // Find the entry with the most specific range .min_by_key(|entry| entry.range.end - entry.range.start) .map(|entry| DiagnosticEntry { diagnostic: entry.diagnostic, range: entry.range.to_anchors(&snapshot.buffer_snapshot), }); // Pull the primary diagnostic out so we can jump to it if the popover is clicked let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| { snapshot .buffer_snapshot .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id) .find(|diagnostic| diagnostic.diagnostic.is_primary) .map(|entry| DiagnosticEntry { diagnostic: entry.diagnostic, range: entry.range.to_anchors(&snapshot.buffer_snapshot), }) }); this.update(&mut cx, |this, _| { this.hover_state.diagnostic_popover = local_diagnostic.map(|local_diagnostic| DiagnosticPopover { local_diagnostic, primary_diagnostic, }); })?; let hovers_response = hover_request.await; let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?; 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 .and_then(|range| { let start = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id, range.start)?; let end = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id, range.end)?; Some(start..end) }) .unwrap_or_else(|| anchor..anchor); let blocks = hover_result.contents; let language = hover_result.language; let parsed_content = parse_blocks(&blocks, &language_registry, language).await; ( 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); } 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 editor.highlight_background::<HoverState>( &hover_highlights, |theme| theme.element_hover, // todo update theme cx, ); } editor.hover_state.info_popovers = info_popovers; cx.notify(); cx.refresh(); })?; anyhow::Ok(()) } .log_err() }); editor.hover_state.info_task = Some(task);} async fn parse_blocks( blocks: &[HoverBlock], language_registry: &Arc<LanguageRegistry>, language: Option<Arc<Language>>,) -> markdown::ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); let mut regions = Vec::new(); for block in blocks { match &block.kind { HoverBlockKind::PlainText => { markdown::new_paragraph(&mut text, &mut Vec::new()); text.push_str(&block.text.replace("\\n", "\n")); } HoverBlockKind::Markdown => { markdown::parse_markdown_block( &block.text.replace("\\n", "\n"), language_registry, language.clone(), &mut text, &mut highlights, &mut region_ranges, &mut regions, ) .await } HoverBlockKind::Code { language } => { if let Some(language) = language_registry .language_for_name(language) .now_or_never() .and_then(Result::ok) { markdown::highlight_code(&mut text, &mut highlights, &block.text, &language); } else { text.push_str(&block.text); } } } } let leading_space = text.chars().take_while(|c| c.is_whitespace()).count(); if leading_space > 0 { highlights = highlights .into_iter() .map(|(range, style)| { ( range.start.saturating_sub(leading_space) ..range.end.saturating_sub(leading_space), style, ) }) .collect(); region_ranges = region_ranges .into_iter() .map(|range| { range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space) }) .collect(); } ParsedMarkdown { text: text.trim().to_string(), highlights, region_ranges, regions, }} #[derive(Default)]pub struct HoverState { pub info_popovers: Vec<InfoPopover>, pub diagnostic_popover: Option<DiagnosticPopover>, pub triggered_from: Option<Anchor>, pub info_task: Option<Task<Option<()>>>,} impl HoverState { pub fn visible(&self) -> bool { !self.info_popovers.is_empty() || self.diagnostic_popover.is_some() } pub fn render( &mut self, snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range<DisplayRow>, max_size: Size<Pixels>, workspace: Option<WeakView<Workspace>>, cx: &mut ViewContext<Editor>, ) -> Option<(DisplayPoint, Vec<AnyElement>)> { // If there is a diagnostic, position the popovers based on that. // Otherwise use the start of the hover range let anchor = self .diagnostic_popover .as_ref() .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) .or_else(|| { 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); // Don't render if the relevant point isn't on screen if !self.visible() || !visible_rows.contains(&point.row()) { return None; } let mut elements = Vec::new(); if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { elements.push(diagnostic_popover.render(style, max_size, cx)); } for info_popover in &mut self.info_popovers { elements.push(info_popover.render(style, max_size, workspace.clone(), cx)); } Some((point, elements)) }} #[derive(Debug, Clone)]pub struct InfoPopover { symbol_range: RangeInEditor, parsed_content: ParsedMarkdown,} impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, max_size: Size<Pixels>,Lines 1–500 of 1418. Load more
use crate::{ display_map::{InlayOffset, ToDisplayPoint}, hover_links::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, Hover, RangeToAnchorExt, }; use futures::{stream::FuturesUnordered, FutureExt}; use gpui::{ div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task, ViewContext, WeakView, }; use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; use lsp::DiagnosticSeverity; use multi_buffer::ToOffset; 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; use workspace::Workspace; pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); pub const HOVER_POPOVER_GAP: Pixels = px(10.); /// Bindable action which uses the most recent selection head to trigger a hover pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) { let head = editor.selections.newest_anchor().head(); show_hover(editor, head, true, cx); } /// The internal hover action dispatches between `show_hover` or `hide_hover` /// depending on whether a point to hover over is provided. pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) { if EditorSettings::get_global(cx).hover_popover_enabled { if let Some(anchor) = anchor { show_hover(editor, anchor, false, cx); } else { hide_hover(editor, cx); } } } pub struct InlayHover { pub excerpt: ExcerptId, pub range: InlayHighlight, pub tooltip: HoverBlock, } pub fn find_hovered_hint_part( label_parts: Vec<InlayHintLabelPart>, hint_start: InlayOffset, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> { if hovered_offset >= hint_start { let mut hovered_character = (hovered_offset - hint_start).0; let mut part_start = hint_start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character > part_len { hovered_character -= part_len; part_start.0 += part_len; } else { let part_end = InlayOffset(part_start.0 + part_len); return Some((part, part_start..part_end)); } } } None } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) { if EditorSettings::get_global(cx).hover_popover_enabled { if editor.pending_rename.is_some() { return; } let Some(project) = editor.project.clone() else { 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); } let task = cx.spawn(|this, mut cx| { async move { cx.background_executor() .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) .await; this.update(&mut cx, |this, _| { this.hover_state.diagnostic_popover = None; })?; let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; let blocks = vec![inlay_hover.tooltip]; let parsed_content = parse_blocks(&blocks, &language_registry, None).await; let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, }; this.update(&mut cx, |this, cx| { // TODO: no background highlights happen for inlays currently this.hover_state.info_popovers = vec![hover_popover]; cx.notify(); })?; anyhow::Ok(()) } .log_err() }); editor.hover_state.info_task = Some(task); } } /// Hides the type information popup. /// 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 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; editor.clear_background_highlights::<HoverState>(cx); if did_hide { cx.notify(); } did_hide } /// Queries the LSP and shows type info and documentation /// about the symbol the mouse is currently hovering over. /// Triggered by the `Hover` action when the cursor may be over a symbol. fn show_hover( editor: &mut Editor, anchor: Anchor, ignore_timeout: bool, cx: &mut ViewContext<Editor>, ) { if editor.pending_rename.is_some() { return; } let snapshot = editor.snapshot(cx); let (buffer, buffer_position) = if let Some(output) = editor.buffer.read(cx).text_anchor_for_position(anchor, cx) { output } else { return; }; let excerpt_id = if let Some((excerpt_id, _, _)) = editor.buffer().read(cx).excerpt_containing(anchor, cx) { excerpt_id } else { return; }; let project = if let Some(project) = editor.project.clone() { project } else { return; }; if !ignore_timeout { 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); let offset = anchor.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(&offset) }) .unwrap_or(false) }) { // Hover triggered from same location as last time. Don't show again. return; } else { hide_hover(editor, cx); } } // Don't request again if the location is the same as the previous request if let Some(triggered_from) = &editor.hover_state.triggered_from { if triggered_from .cmp(&anchor, &snapshot.buffer_snapshot) .is_eq() { return; } } let task = cx.spawn(|this, mut cx| { async move { // If we need to delay, delay a set amount initially before making the lsp request let delay = if ignore_timeout { None } else { // Construct delay task to wait for later let total_delay = Some( cx.background_executor() .timer(Duration::from_millis(HOVER_DELAY_MILLIS)), ); cx.background_executor() .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS)) .await; total_delay }; // query the LSP for hover info let hover_request = cx.update(|cx| { project.update(cx, |project, cx| { project.hover(&buffer, buffer_position, cx) }) })?; if let Some(delay) = delay { delay.await; } // If there's a diagnostic, assign it on the hover state and notify let local_diagnostic = snapshot .buffer_snapshot .diagnostics_in_range::<_, usize>(anchor..anchor, false) // Find the entry with the most specific range .min_by_key(|entry| entry.range.end - entry.range.start) .map(|entry| DiagnosticEntry { diagnostic: entry.diagnostic, range: entry.range.to_anchors(&snapshot.buffer_snapshot), }); // Pull the primary diagnostic out so we can jump to it if the popover is clicked let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| { snapshot .buffer_snapshot .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id) .find(|diagnostic| diagnostic.diagnostic.is_primary) .map(|entry| DiagnosticEntry { diagnostic: entry.diagnostic, range: entry.range.to_anchors(&snapshot.buffer_snapshot), }) }); this.update(&mut cx, |this, _| { this.hover_state.diagnostic_popover = local_diagnostic.map(|local_diagnostic| DiagnosticPopover { local_diagnostic, primary_diagnostic, }); })?; let hovers_response = hover_request.await; let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?; 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 .and_then(|range| { let start = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id, range.start)?; let end = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id, range.end)?; Some(start..end) }) .unwrap_or_else(|| anchor..anchor); let blocks = hover_result.contents; let language = hover_result.language; let parsed_content = parse_blocks(&blocks, &language_registry, language).await; ( 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); } 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 editor.highlight_background::<HoverState>( &hover_highlights, |theme| theme.element_hover, // todo update theme cx, ); } editor.hover_state.info_popovers = info_popovers; cx.notify(); cx.refresh(); })?; anyhow::Ok(()) } .log_err() }); editor.hover_state.info_task = Some(task); } async fn parse_blocks( blocks: &[HoverBlock], language_registry: &Arc<LanguageRegistry>, language: Option<Arc<Language>>, ) -> markdown::ParsedMarkdown { let mut text = String::new(); let mut highlights = Vec::new(); let mut region_ranges = Vec::new(); let mut regions = Vec::new(); for block in blocks { match &block.kind { HoverBlockKind::PlainText => { markdown::new_paragraph(&mut text, &mut Vec::new()); text.push_str(&block.text.replace("\\n", "\n")); } HoverBlockKind::Markdown => { markdown::parse_markdown_block( &block.text.replace("\\n", "\n"), language_registry, language.clone(), &mut text, &mut highlights, &mut region_ranges, &mut regions, ) .await } HoverBlockKind::Code { language } => { if let Some(language) = language_registry .language_for_name(language) .now_or_never() .and_then(Result::ok) { markdown::highlight_code(&mut text, &mut highlights, &block.text, &language); } else { text.push_str(&block.text); } } } } let leading_space = text.chars().take_while(|c| c.is_whitespace()).count(); if leading_space > 0 { highlights = highlights .into_iter() .map(|(range, style)| { ( range.start.saturating_sub(leading_space) ..range.end.saturating_sub(leading_space), style, ) }) .collect(); region_ranges = region_ranges .into_iter() .map(|range| { range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space) }) .collect(); } ParsedMarkdown { text: text.trim().to_string(), highlights, region_ranges, regions, } } #[derive(Default)] pub struct HoverState { pub info_popovers: Vec<InfoPopover>, pub diagnostic_popover: Option<DiagnosticPopover>, pub triggered_from: Option<Anchor>, pub info_task: Option<Task<Option<()>>>, } impl HoverState { pub fn visible(&self) -> bool { !self.info_popovers.is_empty() || self.diagnostic_popover.is_some() } pub fn render( &mut self, snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range<DisplayRow>, max_size: Size<Pixels>, workspace: Option<WeakView<Workspace>>, cx: &mut ViewContext<Editor>, ) -> Option<(DisplayPoint, Vec<AnyElement>)> { // If there is a diagnostic, position the popovers based on that. // Otherwise use the start of the hover range let anchor = self .diagnostic_popover .as_ref() .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) .or_else(|| { 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); // Don't render if the relevant point isn't on screen if !self.visible() || !visible_rows.contains(&point.row()) { return None; } let mut elements = Vec::new(); if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { elements.push(diagnostic_popover.render(style, max_size, cx)); } for info_popover in &mut self.info_popovers { elements.push(info_popover.render(style, max_size, workspace.clone(), cx)); } Some((point, elements)) } } #[derive(Debug, Clone)] pub struct InfoPopover { symbol_range: RangeInEditor, parsed_content: ParsedMarkdown, } impl InfoPopover { pub fn render( &mut self, style: &EditorStyle, max_size: Size<Pixels>,