editor: Add to support HoverPopover. (#1260)

<img width="725" height="631" alt="image"
src="https://github.com/user-attachments/assets/0f4aba90-82cc-43c8-916b-b4a446b8abae"
/>
This commit is contained in:
Jason Lee 2025-09-18 18:18:54 +08:00 committed by GitHub
parent 51efa766ea
commit 6a817d6a05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 559 additions and 106 deletions

View file

@ -14,8 +14,8 @@ use gpui_component::{
h_flex,
highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry},
input::{
self, CodeActionProvider, CompletionProvider, InputEvent, InputState, Position, Rope,
RopeExt, TabSize, TextInput,
self, CodeActionProvider, CompletionProvider, HoverProvider, InputEvent, InputState,
Position, Rope, RopeExt, TabSize, TextInput,
},
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
};
@ -266,6 +266,41 @@ impl CodeActionProvider for ExampleLspStore {
}
}
impl HoverProvider for ExampleLspStore {
fn hover(
&self,
text: &Rope,
offset: usize,
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<Option<lsp_types::Hover>>> {
let word = text.word_at(offset);
if word.is_empty() {
return Task::ready(Ok(None));
}
let Some(item) = self.completions.iter().find(|item| item.label == word) else {
return Task::ready(Ok(None));
};
let contents = if let Some(doc) = &item.documentation {
match doc {
lsp_types::Documentation::String(s) => s.clone(),
lsp_types::Documentation::MarkupContent(mc) => mc.value.clone(),
}
} else {
"No documentation available.".to_string()
};
let hover = lsp_types::Hover {
contents: lsp_types::HoverContents::Scalar(lsp_types::MarkedString::String(contents)),
range: None,
};
Task::ready(Ok(Some(hover)))
}
}
struct TextConvertor;
impl CodeActionProvider for TextConvertor {
@ -489,9 +524,10 @@ impl Example {
.default_value(default_language.1)
.placeholder("Enter your code here...");
editor.lsp.completion_provider = Some(Rc::new(lsp_store.clone()));
editor.lsp.code_action_providers =
vec![Rc::new(lsp_store.clone()), Rc::new(TextConvertor)];
let lsp_store = Rc::new(lsp_store.clone());
editor.lsp.completion_provider = Some(lsp_store.clone());
editor.lsp.code_action_providers = vec![lsp_store.clone(), Rc::new(TextConvertor)];
editor.lsp.hover_provider = Some(lsp_store.clone());
editor
});

View file

@ -390,6 +390,21 @@ impl TextElement {
paths
}
fn layout_hover_highlight(
&self,
last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>,
cx: &mut App,
) -> Option<Path<Pixels>> {
let hover_popover = self.state.read(cx).hover_popover.clone();
let Some(symbol_range) = hover_popover.map(|popover| popover.read(cx).symbol_range.clone())
else {
return None;
};
Self::layout_match_range(symbol_range, last_layout, bounds)
}
fn layout_selections(
&self,
last_layout: &LastLayout,
@ -520,6 +535,7 @@ pub(super) struct PrepaintState {
/// row index (zero based), no wrap, same line as the cursor.
current_row: Option<usize>,
selection_path: Option<Path<Pixels>>,
hover_highlight_path: Option<Path<Pixels>>,
search_match_paths: Vec<(Path<Pixels>, bool)>,
bounds: Bounds<Pixels>,
}
@ -848,6 +864,7 @@ impl Element for TextElement {
let search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx);
let selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
let hover_highlight_path = self.layout_hover_highlight(&last_layout, &mut bounds, cx);
let state = self.state.read(cx);
let line_numbers = if state.mode.line_number() {
@ -907,6 +924,7 @@ impl Element for TextElement {
current_row,
selection_path,
search_match_paths,
hover_highlight_path,
}
}
@ -1003,8 +1021,9 @@ impl Element for TextElement {
// Paint selections
if window.is_window_active() {
let secondary_selection = cx.theme().selection.saturation(0.1);
for (path, is_active) in prepaint.search_match_paths.iter() {
window.paint_path(path.clone(), cx.theme().selection.saturation(0.1));
window.paint_path(path.clone(), secondary_selection);
if *is_active {
window.paint_path(path.clone(), cx.theme().selection);
@ -1014,6 +1033,11 @@ impl Element for TextElement {
if let Some(path) = prepaint.selection_path.take() {
window.paint_path(path, cx.theme().selection);
}
// Paint hover highlight
if let Some(path) = prepaint.hover_highlight_path.take() {
window.paint_path(path, secondary_selection);
}
}
// Paint text

View file

@ -8,14 +8,13 @@ use lsp_types::{
use rope::Rope;
use crate::input::{
popovers::{CodeActionItem, CodeActionMenu, CompletionMenu, ContextMenu},
popovers::{CodeActionItem, CodeActionMenu, CompletionMenu, ContextMenu, HoverPopover},
InputState, RopeExt,
};
/// LSP ServerCapabilities
///
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities
#[derive(Clone, Default)]
pub struct Lsp {
/// The completion provider.
pub completion_provider: Option<Rc<dyn CompletionProvider>>,
@ -23,9 +22,19 @@ pub struct Lsp {
pub code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
/// The hover provider.
pub hover_provider: Option<Rc<dyn HoverProvider>>,
_hover_task: Task<Result<()>>,
}
impl Lsp {}
impl Default for Lsp {
fn default() -> Self {
Self {
completion_provider: None,
code_action_providers: vec![],
hover_provider: None,
_hover_task: Task::ready(Ok(())),
}
}
}
/// A trait for providing code completions based on the current input state and context.
pub trait CompletionProvider {
@ -351,4 +360,42 @@ impl InputState {
self.replace_text_in_range(Some(range_utf16), &edit.new_text, window, cx);
}
}
/// Handle hover trigger LSP request.
pub(super) fn handle_hover(
&mut self,
offset: usize,
window: &mut Window,
cx: &mut Context<InputState>,
) {
let Some(provider) = self.lsp.hover_provider.clone() else {
return;
};
if let Some(hover_popover) = self.hover_popover.as_ref() {
if hover_popover.read(cx).is_same(offset) {
return;
}
}
// Currently not implemented.
let task = provider.hover(&self.text, offset, window, cx);
let word_range = self.text.word_range(offset).unwrap_or(offset..offset);
let editor = cx.entity();
self.lsp._hover_task = cx.spawn_in(window, async move |_, cx| {
let result = task.await?;
_ = editor.update(cx, |editor, cx| match result {
Some(hover) => {
let hover_popover = HoverPopover::new(cx.entity(), word_range, &hover, cx);
editor.hover_popover = Some(hover_popover);
}
None => {
editor.hover_popover = None;
}
});
Ok(())
});
}
}

View file

@ -1,10 +1,10 @@
use std::rc::Rc;
use gpui::{
canvas, deferred, div, prelude::FluentBuilder, px, relative, rems, Action, AnyElement, App,
AppContext, Bounds, Context, DismissEvent, Div, ElementId, Empty, Entity, EntityInputHandler,
EventEmitter, HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels,
Point, Render, RenderOnce, SharedString, Stateful, Styled, StyledText, Subscription, Window,
canvas, deferred, div, prelude::FluentBuilder, px, relative, Action, AnyElement, App,
AppContext, Bounds, Context, DismissEvent, Empty, Entity, EntityInputHandler, EventEmitter,
HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render,
RenderOnce, SharedString, Styled, StyledText, Subscription, Window,
};
use lsp_types::CompletionItem;
@ -14,10 +14,13 @@ const POPOVER_GAP: Pixels = px(4.);
use crate::{
actions, h_flex,
input::{self, InputState},
input::{
self,
popovers::{popover, render_markdown},
InputState,
},
label::Label,
list::{List, ListDelegate, ListEvent},
text::{TextView, TextViewStyle},
ActiveTheme, IndexPath, Selectable,
};
@ -382,21 +385,6 @@ impl Render for CompletionMenu {
.selected_item()
.and_then(|item| item.documentation.clone());
fn popover(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
div()
.id(id)
.flex_none()
.occlude()
.p_1()
.text_xs()
.text_color(cx.theme().popover_foreground)
.bg(cx.theme().popover)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.shadow_md()
}
let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x);
let vertical_layout = pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP
> window.bounds().size.width;
@ -439,19 +427,7 @@ impl Render for CompletionMenu {
popover("completion-menu", cx)
.w(MAX_MENU_WIDTH)
.px_2()
.child(
TextView::markdown("doc", doc, window, cx)
.style(
TextViewStyle::default()
.paragraph_gap(rems(0.5))
.heading_font_size(|level, rem_size| match level {
1..=3 => rem_size * 1,
4 => rem_size * 0.9,
_ => rem_size * 0.8,
}),
)
.selectable(),
),
.child(render_markdown("doc", doc, window, cx)),
),
)
})

View file

@ -1,11 +1,17 @@
use std::rc::Rc;
use gpui::{
canvas, deferred, div, px, App, AppContext as _, Bounds, Context, Empty, Entity,
InteractiveElement, IntoElement, ParentElement as _, Pixels, Point, Render, Styled, Window,
prelude::FluentBuilder as _, px, App, AppContext as _, Bounds, Context, Empty, Entity,
IntoElement, Pixels, Point, Render, Styled, Window,
};
use crate::{highlighter::DiagnosticEntry, input::InputState, text::TextView, ActiveTheme as _};
use crate::{
highlighter::DiagnosticEntry,
input::{
popovers::{render_markdown, Popover},
InputState,
},
};
pub struct DiagnosticPopover {
state: Entity<InputState>,
@ -30,18 +36,6 @@ impl DiagnosticPopover {
})
}
fn origin(&self, cx: &App) -> Option<Point<Pixels>> {
let state = self.state.read(cx);
let Some(last_layout) = state.last_layout.as_ref() else {
return None;
};
let line_number_width = last_layout.line_number_width;
let (_, _, start_pos) = state.line_and_position_for_offset(self.diagnostic.range.start);
start_pos.map(|pos| pos + Point::new(line_number_width, px(0.)))
}
pub(crate) fn show(&mut self, cx: &mut Context<Self>) {
self.open = true;
cx.notify();
@ -70,61 +64,32 @@ impl DiagnosticPopover {
}
impl Render for DiagnosticPopover {
fn render(&mut self, window: &mut Window, cx: &mut gpui::Context<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.open {
return Empty.into_any_element();
}
let view = cx.entity();
let message = self.diagnostic.message.clone();
let Some(pos) = self.origin(cx) else {
return Empty.into_any_element();
};
let (border, bg, fg) = (
self.diagnostic.severity.border(cx),
self.diagnostic.severity.bg(cx),
self.diagnostic.severity.fg(cx),
);
let scroll_origin = self.state.read(cx).scroll_handle.offset();
let y = pos.y - self.bounds.size.height + scroll_origin.y;
let x = pos.x + scroll_origin.x;
let max_width = px(500.).min(window.bounds().size.width - x);
deferred(
div()
.id("diagnostic-popover")
.absolute()
.left(x)
.top(y)
.px_1()
.py_0p5()
.text_xs()
.max_w(max_width)
.bg(bg)
.text_color(fg)
.border_1()
.border_color(border)
.rounded(cx.theme().radius)
.shadow_md()
.child(TextView::markdown("message", message, window, cx).selectable())
.child(
canvas(
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _, _| {},
)
.top_0()
.left_0()
.absolute()
.size_full(),
)
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
this.open = false;
cx.notify();
})),
Popover::new(
"diagnostic-popover",
self.state.clone(),
self.diagnostic.range.clone(),
move |window, cx| render_markdown("message", message.clone(), window, cx),
)
.when(!self.open, |this| this.invisible())
.px_1()
.py_0p5()
.bg(bg)
.text_color(fg)
.border_1()
.border_color(border)
.into_any_element()
}
}

View file

@ -0,0 +1,287 @@
use std::{ops::Range, rc::Rc};
use gpui::{
deferred, div, point, prelude::FluentBuilder as _, px, AnyElement, App, AppContext as _,
AvailableSpace, Bounds, Element, ElementId, Entity, InteractiveElement, IntoElement,
MouseDownEvent, ParentElement as _, Pixels, Render, StyleRefinement, Styled, Window,
};
use crate::{
input::{popovers::render_markdown, InputState},
ActiveTheme as _, StyledExt,
};
pub struct HoverPopover {
editor: Entity<InputState>,
/// The symbol range byte of the hover trigger.
pub(crate) symbol_range: Range<usize>,
pub(crate) hover: Rc<lsp_types::Hover>,
}
impl HoverPopover {
pub fn new(
editor: Entity<InputState>,
symbol_range: Range<usize>,
hover: &lsp_types::Hover,
cx: &mut App,
) -> Entity<Self> {
let hover = Rc::new(hover.clone());
cx.new(|_| Self {
editor,
symbol_range,
hover,
})
}
pub(crate) fn is_same(&self, offset: usize) -> bool {
self.symbol_range.contains(&offset)
}
}
impl Render for HoverPopover {
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<Self>) -> impl IntoElement {
let contents = match self.hover.contents.clone() {
lsp_types::HoverContents::Scalar(scalar) => match scalar {
lsp_types::MarkedString::String(s) => s,
lsp_types::MarkedString::LanguageString(ls) => ls.value,
},
lsp_types::HoverContents::Array(arr) => arr
.into_iter()
.map(|item| match item {
lsp_types::MarkedString::String(s) => s,
lsp_types::MarkedString::LanguageString(ls) => ls.value,
})
.collect::<Vec<_>>()
.join("\n\n"),
lsp_types::HoverContents::Markup(markup) => markup.value,
};
Popover::new(
"hover-popover",
self.editor.clone(),
self.symbol_range.clone(),
move |window, cx| render_markdown("message", contents.clone(), window, cx),
)
.into_any_element()
}
}
pub(crate) struct Popover {
id: ElementId,
style: StyleRefinement,
editor: Entity<InputState>,
range: Range<usize>,
width_limit: Range<Pixels>,
content_builder: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl Styled for Popover {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl Popover {
pub fn new<F, E>(
id: impl Into<ElementId>,
editor: Entity<InputState>,
range: Range<usize>,
f: F,
) -> Self
where
F: Fn(&mut Window, &mut App) -> E + 'static,
E: IntoElement,
{
Self {
id: id.into(),
editor,
range,
style: StyleRefinement::default(),
width_limit: px(200.)..px(500.),
content_builder: Box::new(move |window, cx| (f)(window, cx).into_any_element()),
}
}
/// Get the bounds of the range in the editor, if it is visible.
fn trigger_bounds(&self, cx: &App) -> Option<Bounds<Pixels>> {
let editor = self.editor.read(cx);
let Some(last_layout) = editor.last_layout.as_ref() else {
return None;
};
let Some(last_bounds) = editor.last_bounds else {
return None;
};
let (_, _, start_pos) = editor.line_and_position_for_offset(self.range.start);
let (_, _, end_pos) = editor.line_and_position_for_offset(self.range.end);
let Some(start_pos) = start_pos else {
return None;
};
let Some(end_pos) = end_pos else {
return None;
};
Some(Bounds::from_corners(
last_bounds.origin + start_pos,
last_bounds.origin + end_pos + point(px(0.), last_layout.line_height),
))
}
}
impl IntoElement for Popover {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
pub(crate) struct PopoverLayoutState {
state: Entity<bool>,
bounds: Bounds<Pixels>,
element: Option<AnyElement>,
}
impl Element for Popover {
type RequestLayoutState = PopoverLayoutState;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let open_state = window.use_keyed_state("popover-open", cx, |_, _| true);
let trigger_bounds = match self.trigger_bounds(cx) {
Some(bounds) => bounds,
None => {
return (
div().into_any_element().request_layout(window, cx),
PopoverLayoutState {
bounds: Bounds::default(),
element: None,
state: open_state,
},
)
}
};
let max_width = self
.width_limit
.end
.min(window.bounds().size.width - SNAP_TO_EDGE * 2)
.max(px(200.));
let is_open = *open_state.read(cx);
let mut popover = deferred(
div()
.when(!is_open, |s| s.invisible())
.flex_none()
.occlude()
.p_1()
.text_xs()
.text_color(cx.theme().popover_foreground)
.bg(cx.theme().popover)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.shadow_md()
.max_w(max_width)
.refine_style(&self.style)
.child((self.content_builder)(window, cx)),
)
.into_any_element();
let popover_size = popover.layout_as_root(AvailableSpace::min_size(), window, cx);
const SNAP_TO_EDGE: Pixels = px(8.);
let top_space = trigger_bounds.top() - SNAP_TO_EDGE;
let right_space = window.bounds().size.width - trigger_bounds.left() - SNAP_TO_EDGE;
let mut pos = point(
trigger_bounds.left(),
trigger_bounds.top() - popover_size.height,
);
if popover_size.height > top_space {
pos.y = trigger_bounds.bottom();
}
if popover_size.width > right_space {
pos.x = trigger_bounds.right() - popover_size.width;
}
let mut empty = div().into_any_element();
let layout_id = empty.request_layout(window, cx);
(
layout_id,
PopoverLayoutState {
bounds: Bounds {
origin: pos,
size: popover_size,
},
element: Some(popover),
state: open_state,
},
)
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
let bounds = request_layout.bounds;
let Some(popover) = request_layout.element.as_mut() else {
return;
};
window.with_absolute_element_offset(bounds.origin, |window| {
popover.prepaint(window, cx);
})
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
let bounds = request_layout.bounds;
let Some(popover) = request_layout.element.as_mut() else {
return;
};
popover.paint(window, cx);
let open_state = request_layout.state.clone();
// Mouse down out to hide.
window.on_mouse_event(move |event: &MouseDownEvent, _, _, cx| {
if !bounds.contains(&event.position) {
open_state.update(cx, |open, cx| {
*open = false;
cx.notify();
})
}
})
}
}

View file

@ -1,11 +1,22 @@
mod code_action_menu;
mod completion_menu;
mod diagnostic_popover;
mod hover_popover;
pub(crate) use code_action_menu::*;
pub(crate) use completion_menu::*;
pub(crate) use diagnostic_popover::*;
use gpui::{App, Entity, IntoElement};
pub(crate) use hover_popover::*;
use gpui::{
div, rems, App, Div, ElementId, Entity, InteractiveElement as _, IntoElement, SharedString,
Stateful, Styled as _, Window,
};
use crate::{
text::{TextView, TextViewStyle},
ActiveTheme as _,
};
pub(crate) enum ContextMenu {
Completion(Entity<CompletionMenu>),
@ -27,3 +38,37 @@ impl ContextMenu {
}
}
}
pub(super) fn render_markdown(
id: impl Into<ElementId>,
markdown: impl Into<SharedString>,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
TextView::markdown(id, markdown, window, cx)
.style(
TextViewStyle::default()
.paragraph_gap(rems(0.5))
.heading_font_size(|level, rem_size| match level {
1..=3 => rem_size * 1,
4 => rem_size * 0.9,
_ => rem_size * 0.8,
}),
)
.selectable()
}
pub(super) fn popover(id: impl Into<ElementId>, cx: &App) -> Stateful<Div> {
div()
.id(id)
.flex_none()
.occlude()
.p_1()
.text_xs()
.text_color(cx.theme().popover_foreground)
.bg(cx.theme().popover)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.shadow_md()
}

View file

@ -1,3 +1,5 @@
use std::ops::Range;
use rope::{Point, Rope};
use crate::input::Position;
@ -43,6 +45,12 @@ pub trait RopeExt {
/// Get the line, column [`Position`] (0-based) from the given byte offset.
fn offset_to_position(&self, offset: usize) -> Position;
/// Get the word byte range at the given offset (byte).
fn word_range(&self, offset: usize) -> Option<Range<usize>>;
/// Get word at the given offset (byte).
fn word_at(&self, offset: usize) -> String;
}
/// An iterator over the lines of a `Rope`.
@ -152,8 +160,48 @@ impl RopeExt for Rope {
return None;
}
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
self.slice(offset..self.len()).chars().next()
}
fn word_range(&self, offset: usize) -> Option<Range<usize>> {
if offset >= self.len() {
return None;
}
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
let mut left = String::new();
for c in self.reversed_chars_at(offset) {
if c.is_alphanumeric() || c == '_' {
left.insert(0, c);
} else {
break;
}
}
let start = offset.saturating_sub(left.len());
let right = self
.chars_at(offset)
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect::<String>();
let end = offset + right.len();
if start == end {
None
} else {
Some(start..end)
}
}
fn word_at(&self, offset: usize) -> String {
if let Some(range) = self.word_range(offset) {
self.slice(range).to_string()
} else {
String::new()
}
}
}
#[cfg(test)]
@ -267,4 +315,24 @@ mod tests {
assert_eq!(rope.char_at(38), Some('\n'));
assert_eq!(rope.char_at(50), None);
}
#[test]
fn test_word_at() {
let rope = Rope::from("Hello\nWorld\r\nThis is a test 中文 世界\nRope");
assert_eq!(rope.word_at(0), "Hello");
assert_eq!(rope.word_range(0), Some(0..5));
assert_eq!(rope.word_at(8), "World");
assert_eq!(rope.word_range(8), Some(6..11));
assert_eq!(rope.word_at(12), "");
assert_eq!(rope.word_range(12), None);
assert_eq!(rope.word_at(13), "This");
assert_eq!(rope.word_range(13), Some(13..17));
assert_eq!(rope.word_at(31), "中文");
assert_eq!(rope.word_range(31), Some(28..34));
assert_eq!(rope.word_at(38), "世界");
assert_eq!(rope.word_range(38), Some(35..41));
assert_eq!(rope.word_at(44), "Rope");
assert_eq!(rope.word_range(44), Some(42..46));
assert_eq!(rope.word_at(45), "Rope");
}
}

View file

@ -30,7 +30,7 @@ use super::{
text_wrapper::TextWrapper,
};
use crate::input::{
popovers::{ContextMenu, DiagnosticPopover},
popovers::{ContextMenu, DiagnosticPopover, HoverPopover},
search::{self, SearchPanel},
Lsp, Position,
};
@ -298,6 +298,7 @@ pub struct InputState {
pub(super) context_menu: Option<ContextMenu>,
/// A flag to indicate if we are currently inserting a completion item.
pub(super) completion_inserting: bool,
pub(super) hover_popover: Option<Entity<HoverPopover>>,
pub lsp: Lsp,
@ -382,6 +383,7 @@ impl InputState {
diagnostic_popover: None,
context_menu: None,
completion_inserting: false,
hover_popover: None,
_subscriptions,
_context_menu_task: Task::ready(Ok(())),
}
@ -599,7 +601,7 @@ impl InputState {
let local_offset = offset.saturating_sub(prev_lines_offset);
if let Some(pos) = line.position_for_index(local_offset, line_height) {
let sub_line_index = (pos.y.0 / line_height.0) as usize;
let adjusted_pos = point(pos.x, pos.y + y_offset);
let adjusted_pos = point(pos.x + last_layout.line_number_width, pos.y + y_offset);
return (line_index, sub_line_index, Some(adjusted_pos));
}
@ -1573,9 +1575,11 @@ impl InputState {
window: &mut Window,
cx: &mut Context<Self>,
) {
// Show diagnostic popover on mouse move
let offset = self.index_for_mouse_position(event.position, window, cx);
self.handle_hover(offset, window, cx);
if self.mode.is_code_editor() {
// Show diagnostic popover on mouse move
let offset = self.index_for_mouse_position(event.position, window, cx);
if let Some(diagnostic) = self
.mode
.diagnostics()
@ -2433,5 +2437,6 @@ impl Render for InputState {
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
.children(self.diagnostic_popover.clone())
.children(self.context_menu.as_ref().map(|menu| menu.render()))
.children(self.hover_popover.clone())
}
}