use crate::{ display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint}, hover_links::{InlayHighlight, RangeInEditor}, scroll::ScrollAmount, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, Hover, RangeToAnchorExt,};use gpui::{ div, px, AnyElement, AsyncWindowContext, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext,};use itertools::Itertools;use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry};use lsp::DiagnosticSeverity;use markdown::{Markdown, MarkdownStyle};use multi_buffer::ToOffset;use project::{HoverBlock, InlayHintLabelPart};use settings::Settings;use std::rc::Rc;use std::{borrow::Cow, cell::RefCell};use std::{ops::Range, sync::Arc, time::Duration};use theme::ThemeSettings;use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};use util::TryFutureExt;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 show_keyboard_hover(editor, cx) { return; } if let Some(anchor) = anchor { show_hover(editor, anchor, false, cx); } else { hide_hover(editor, cx); } }} pub fn show_keyboard_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool { let info_popovers = editor.hover_state.info_popovers.clone(); for p in info_popovers { let keyboard_grace = p.keyboard_grace.borrow(); if *keyboard_grace { if let Some(anchor) = p.anchor { show_hover(editor, anchor, false, cx); return true; } } } let diagnostic_popover = editor.hover_state.diagnostic_popover.clone(); if let Some(d) = diagnostic_popover { let keyboard_grace = d.keyboard_grace.borrow(); if *keyboard_grace { if let Some(anchor) = d.anchor { show_hover(editor, anchor, false, cx); return true; } } } false} pub struct InlayHover { 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, &mut cx).await; let scroll_handle = ScrollHandle::new(); let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(false)), anchor: None, }; 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>,) -> Option<()> { if editor.pending_rename.is_some() { return None; } let snapshot = editor.snapshot(cx); let (buffer, buffer_position) = editor .buffer .read(cx) .text_anchor_for_position(anchor, cx)?; let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; let language_registry = editor.project.as_ref()?.read(cx).languages().clone(); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { if same_info_hover(editor, &snapshot, anchor) || same_diagnostic_hover(editor, &snapshot, anchor) || editor.hover_state.diagnostic_popover.is_some() { // Hover triggered from same location as last time. Don't show again. return None; } 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 None; } } 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 }; let hover_request = cx.update(|cx| provider.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 mut 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), }) }); if let Some(invisible) = snapshot .buffer_snapshot .chars_at(anchor) .next() .filter(|&c| is_invisible(c)) { let after = snapshot.buffer_snapshot.anchor_after( anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(), ); local_diagnostic = Some(DiagnosticEntry { diagnostic: Diagnostic { severity: DiagnosticSeverity::HINT, message: format!("Unicode character U+{:02X}", invisible as u32), ..Default::default() }, range: anchor..after, }) } else if let Some(invisible) = snapshot .buffer_snapshot .reversed_chars_at(anchor) .next() .filter(|&c| is_invisible(c)) { let before = snapshot.buffer_snapshot.anchor_before( anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(), ); local_diagnostic = Some(DiagnosticEntry { diagnostic: Diagnostic { severity: DiagnosticSeverity::HINT, message: format!("Unicode character U+{:02X}", invisible as u32), ..Default::default() }, range: before..anchor, }) } let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic { let text = match local_diagnostic.diagnostic.source { Some(ref source) => { format!("{source}: {}", local_diagnostic.diagnostic.message) } None => local_diagnostic.diagnostic.message.clone(), }; let mut border_color: Option<Hsla> = None; let mut background_color: Option<Hsla> = None; let parsed_content = cx .new_view(|cx| { let status_colors = cx.theme().status(); match local_diagnostic.diagnostic.severity { DiagnosticSeverity::ERROR => { background_color = Some(status_colors.error_background); border_color = Some(status_colors.error_border); } DiagnosticSeverity::WARNING => { background_color = Some(status_colors.warning_background); border_color = Some(status_colors.warning_border); } DiagnosticSeverity::INFORMATION => { background_color = Some(status_colors.info_background); border_color = Some(status_colors.info_border); } DiagnosticSeverity::HINT => { background_color = Some(status_colors.hint_background); border_color = Some(status_colors.hint_border); } _ => { background_color = Some(status_colors.ignored_background); border_color = Some(status_colors.ignored_border); } }; let settings = ThemeSettings::get_global(cx); let mut base_text_style = cx.text_style(); base_text_style.refine(&TextStyleRefinement { font_family: Some(settings.ui_font.family.clone()), font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: Some(settings.ui_font_size.into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(gpui::transparent_black()), ..Default::default() }); let markdown_style = MarkdownStyle { base_text_style, selection_background_color: { cx.theme().players().local().selection }, link: TextStyleRefinement { underline: Some(gpui::UnderlineStyle { thickness: px(1.), color: Some(cx.theme().colors().editor_foreground), wavy: false, }), ..Default::default() }, ..Default::default() }; Markdown::new_text(text, markdown_style.clone(), None, None, cx) }) .ok(); Some(DiagnosticPopover { local_diagnostic, primary_diagnostic, parsed_content, border_color, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), }) } else { None }; this.update(&mut cx, |this, _| { this.hover_state.diagnostic_popover = diagnostic_popover; })?; let hovers_response = if let Some(hover_request) = hover_request { hover_request.await } else { Vec::new() }; 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 = Vec::with_capacity(hovers_response.len()); for hover_result in hovers_response { // 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) }) .or_else(|| { let snapshot = &snapshot.buffer_snapshot; let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?; Some( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.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, &mut cx).await; let scroll_handle = ScrollHandle::new(); info_popover_tasks.push(( range.clone(), InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), }, )); } for (highlight_range, info_popover) in info_popover_tasks { 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); None} fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool { 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)Lines 1–500 of 1765. Load more
use crate::{ display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint}, hover_links::{InlayHighlight, RangeInEditor}, scroll::ScrollAmount, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, Hover, RangeToAnchorExt, }; use gpui::{ div, px, AnyElement, AsyncWindowContext, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement, View, ViewContext, }; use itertools::Itertools; use language::{Diagnostic, DiagnosticEntry, Language, LanguageRegistry}; use lsp::DiagnosticSeverity; use markdown::{Markdown, MarkdownStyle}; use multi_buffer::ToOffset; use project::{HoverBlock, InlayHintLabelPart}; use settings::Settings; use std::rc::Rc; use std::{borrow::Cow, cell::RefCell}; use std::{ops::Range, sync::Arc, time::Duration}; use theme::ThemeSettings; use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState}; use util::TryFutureExt; 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 show_keyboard_hover(editor, cx) { return; } if let Some(anchor) = anchor { show_hover(editor, anchor, false, cx); } else { hide_hover(editor, cx); } } } pub fn show_keyboard_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool { let info_popovers = editor.hover_state.info_popovers.clone(); for p in info_popovers { let keyboard_grace = p.keyboard_grace.borrow(); if *keyboard_grace { if let Some(anchor) = p.anchor { show_hover(editor, anchor, false, cx); return true; } } } let diagnostic_popover = editor.hover_state.diagnostic_popover.clone(); if let Some(d) = diagnostic_popover { let keyboard_grace = d.keyboard_grace.borrow(); if *keyboard_grace { if let Some(anchor) = d.anchor { show_hover(editor, anchor, false, cx); return true; } } } false } pub struct InlayHover { 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, &mut cx).await; let scroll_handle = ScrollHandle::new(); let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(false)), anchor: None, }; 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>, ) -> Option<()> { if editor.pending_rename.is_some() { return None; } let snapshot = editor.snapshot(cx); let (buffer, buffer_position) = editor .buffer .read(cx) .text_anchor_for_position(anchor, cx)?; let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?; let language_registry = editor.project.as_ref()?.read(cx).languages().clone(); let provider = editor.semantics_provider.clone()?; if !ignore_timeout { if same_info_hover(editor, &snapshot, anchor) || same_diagnostic_hover(editor, &snapshot, anchor) || editor.hover_state.diagnostic_popover.is_some() { // Hover triggered from same location as last time. Don't show again. return None; } 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 None; } } 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 }; let hover_request = cx.update(|cx| provider.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 mut 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), }) }); if let Some(invisible) = snapshot .buffer_snapshot .chars_at(anchor) .next() .filter(|&c| is_invisible(c)) { let after = snapshot.buffer_snapshot.anchor_after( anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(), ); local_diagnostic = Some(DiagnosticEntry { diagnostic: Diagnostic { severity: DiagnosticSeverity::HINT, message: format!("Unicode character U+{:02X}", invisible as u32), ..Default::default() }, range: anchor..after, }) } else if let Some(invisible) = snapshot .buffer_snapshot .reversed_chars_at(anchor) .next() .filter(|&c| is_invisible(c)) { let before = snapshot.buffer_snapshot.anchor_before( anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(), ); local_diagnostic = Some(DiagnosticEntry { diagnostic: Diagnostic { severity: DiagnosticSeverity::HINT, message: format!("Unicode character U+{:02X}", invisible as u32), ..Default::default() }, range: before..anchor, }) } let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic { let text = match local_diagnostic.diagnostic.source { Some(ref source) => { format!("{source}: {}", local_diagnostic.diagnostic.message) } None => local_diagnostic.diagnostic.message.clone(), }; let mut border_color: Option<Hsla> = None; let mut background_color: Option<Hsla> = None; let parsed_content = cx .new_view(|cx| { let status_colors = cx.theme().status(); match local_diagnostic.diagnostic.severity { DiagnosticSeverity::ERROR => { background_color = Some(status_colors.error_background); border_color = Some(status_colors.error_border); } DiagnosticSeverity::WARNING => { background_color = Some(status_colors.warning_background); border_color = Some(status_colors.warning_border); } DiagnosticSeverity::INFORMATION => { background_color = Some(status_colors.info_background); border_color = Some(status_colors.info_border); } DiagnosticSeverity::HINT => { background_color = Some(status_colors.hint_background); border_color = Some(status_colors.hint_border); } _ => { background_color = Some(status_colors.ignored_background); border_color = Some(status_colors.ignored_border); } }; let settings = ThemeSettings::get_global(cx); let mut base_text_style = cx.text_style(); base_text_style.refine(&TextStyleRefinement { font_family: Some(settings.ui_font.family.clone()), font_fallbacks: settings.ui_font.fallbacks.clone(), font_size: Some(settings.ui_font_size.into()), color: Some(cx.theme().colors().editor_foreground), background_color: Some(gpui::transparent_black()), ..Default::default() }); let markdown_style = MarkdownStyle { base_text_style, selection_background_color: { cx.theme().players().local().selection }, link: TextStyleRefinement { underline: Some(gpui::UnderlineStyle { thickness: px(1.), color: Some(cx.theme().colors().editor_foreground), wavy: false, }), ..Default::default() }, ..Default::default() }; Markdown::new_text(text, markdown_style.clone(), None, None, cx) }) .ok(); Some(DiagnosticPopover { local_diagnostic, primary_diagnostic, parsed_content, border_color, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), }) } else { None }; this.update(&mut cx, |this, _| { this.hover_state.diagnostic_popover = diagnostic_popover; })?; let hovers_response = if let Some(hover_request) = hover_request { hover_request.await } else { Vec::new() }; 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 = Vec::with_capacity(hovers_response.len()); for hover_result in hovers_response { // 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) }) .or_else(|| { let snapshot = &snapshot.buffer_snapshot; let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?; Some( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.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, &mut cx).await; let scroll_handle = ScrollHandle::new(); info_popover_tasks.push(( range.clone(), InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), }, )); } for (highlight_range, info_popover) in info_popover_tasks { 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); None } fn same_info_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anchor) -> bool { 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)