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:
parent
51efa766ea
commit
6a817d6a05
9 changed files with 559 additions and 106 deletions
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
287
crates/ui/src/input/popovers/hover_popover.rs
Normal file
287
crates/ui/src/input/popovers/hover_popover.rs
Normal 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();
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue