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,
|
h_flex,
|
||||||
highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry},
|
highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry},
|
||||||
input::{
|
input::{
|
||||||
self, CodeActionProvider, CompletionProvider, InputEvent, InputState, Position, Rope,
|
self, CodeActionProvider, CompletionProvider, HoverProvider, InputEvent, InputState,
|
||||||
RopeExt, TabSize, TextInput,
|
Position, Rope, RopeExt, TabSize, TextInput,
|
||||||
},
|
},
|
||||||
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable,
|
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;
|
struct TextConvertor;
|
||||||
|
|
||||||
impl CodeActionProvider for TextConvertor {
|
impl CodeActionProvider for TextConvertor {
|
||||||
|
|
@ -489,9 +524,10 @@ impl Example {
|
||||||
.default_value(default_language.1)
|
.default_value(default_language.1)
|
||||||
.placeholder("Enter your code here...");
|
.placeholder("Enter your code here...");
|
||||||
|
|
||||||
editor.lsp.completion_provider = Some(Rc::new(lsp_store.clone()));
|
let lsp_store = Rc::new(lsp_store.clone());
|
||||||
editor.lsp.code_action_providers =
|
editor.lsp.completion_provider = Some(lsp_store.clone());
|
||||||
vec![Rc::new(lsp_store.clone()), Rc::new(TextConvertor)];
|
editor.lsp.code_action_providers = vec![lsp_store.clone(), Rc::new(TextConvertor)];
|
||||||
|
editor.lsp.hover_provider = Some(lsp_store.clone());
|
||||||
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,21 @@ impl TextElement {
|
||||||
paths
|
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(
|
fn layout_selections(
|
||||||
&self,
|
&self,
|
||||||
last_layout: &LastLayout,
|
last_layout: &LastLayout,
|
||||||
|
|
@ -520,6 +535,7 @@ pub(super) struct PrepaintState {
|
||||||
/// row index (zero based), no wrap, same line as the cursor.
|
/// row index (zero based), no wrap, same line as the cursor.
|
||||||
current_row: Option<usize>,
|
current_row: Option<usize>,
|
||||||
selection_path: Option<Path<Pixels>>,
|
selection_path: Option<Path<Pixels>>,
|
||||||
|
hover_highlight_path: Option<Path<Pixels>>,
|
||||||
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
search_match_paths: Vec<(Path<Pixels>, bool)>,
|
||||||
bounds: Bounds<Pixels>,
|
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 search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx);
|
||||||
let selection_path = self.layout_selections(&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 state = self.state.read(cx);
|
||||||
let line_numbers = if state.mode.line_number() {
|
let line_numbers = if state.mode.line_number() {
|
||||||
|
|
@ -907,6 +924,7 @@ impl Element for TextElement {
|
||||||
current_row,
|
current_row,
|
||||||
selection_path,
|
selection_path,
|
||||||
search_match_paths,
|
search_match_paths,
|
||||||
|
hover_highlight_path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1003,8 +1021,9 @@ impl Element for TextElement {
|
||||||
|
|
||||||
// Paint selections
|
// Paint selections
|
||||||
if window.is_window_active() {
|
if window.is_window_active() {
|
||||||
|
let secondary_selection = cx.theme().selection.saturation(0.1);
|
||||||
for (path, is_active) in prepaint.search_match_paths.iter() {
|
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 {
|
if *is_active {
|
||||||
window.paint_path(path.clone(), cx.theme().selection);
|
window.paint_path(path.clone(), cx.theme().selection);
|
||||||
|
|
@ -1014,6 +1033,11 @@ impl Element for TextElement {
|
||||||
if let Some(path) = prepaint.selection_path.take() {
|
if let Some(path) = prepaint.selection_path.take() {
|
||||||
window.paint_path(path, cx.theme().selection);
|
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
|
// Paint text
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,13 @@ use lsp_types::{
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
|
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
popovers::{CodeActionItem, CodeActionMenu, CompletionMenu, ContextMenu},
|
popovers::{CodeActionItem, CodeActionMenu, CompletionMenu, ContextMenu, HoverPopover},
|
||||||
InputState, RopeExt,
|
InputState, RopeExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// LSP ServerCapabilities
|
/// LSP ServerCapabilities
|
||||||
///
|
///
|
||||||
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities
|
/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct Lsp {
|
pub struct Lsp {
|
||||||
/// The completion provider.
|
/// The completion provider.
|
||||||
pub completion_provider: Option<Rc<dyn CompletionProvider>>,
|
pub completion_provider: Option<Rc<dyn CompletionProvider>>,
|
||||||
|
|
@ -23,9 +22,19 @@ pub struct Lsp {
|
||||||
pub code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
pub code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
||||||
/// The hover provider.
|
/// The hover provider.
|
||||||
pub hover_provider: Option<Rc<dyn HoverProvider>>,
|
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.
|
/// A trait for providing code completions based on the current input state and context.
|
||||||
pub trait CompletionProvider {
|
pub trait CompletionProvider {
|
||||||
|
|
@ -351,4 +360,42 @@ impl InputState {
|
||||||
self.replace_text_in_range(Some(range_utf16), &edit.new_text, window, cx);
|
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 std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, deferred, div, prelude::FluentBuilder, px, relative, rems, Action, AnyElement, App,
|
canvas, deferred, div, prelude::FluentBuilder, px, relative, Action, AnyElement, App,
|
||||||
AppContext, Bounds, Context, DismissEvent, Div, ElementId, Empty, Entity, EntityInputHandler,
|
AppContext, Bounds, Context, DismissEvent, Empty, Entity, EntityInputHandler, EventEmitter,
|
||||||
EventEmitter, HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels,
|
HighlightStyle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render,
|
||||||
Point, Render, RenderOnce, SharedString, Stateful, Styled, StyledText, Subscription, Window,
|
RenderOnce, SharedString, Styled, StyledText, Subscription, Window,
|
||||||
};
|
};
|
||||||
use lsp_types::CompletionItem;
|
use lsp_types::CompletionItem;
|
||||||
|
|
||||||
|
|
@ -14,10 +14,13 @@ const POPOVER_GAP: Pixels = px(4.);
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions, h_flex,
|
actions, h_flex,
|
||||||
input::{self, InputState},
|
input::{
|
||||||
|
self,
|
||||||
|
popovers::{popover, render_markdown},
|
||||||
|
InputState,
|
||||||
|
},
|
||||||
label::Label,
|
label::Label,
|
||||||
list::{List, ListDelegate, ListEvent},
|
list::{List, ListDelegate, ListEvent},
|
||||||
text::{TextView, TextViewStyle},
|
|
||||||
ActiveTheme, IndexPath, Selectable,
|
ActiveTheme, IndexPath, Selectable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -382,21 +385,6 @@ impl Render for CompletionMenu {
|
||||||
.selected_item()
|
.selected_item()
|
||||||
.and_then(|item| item.documentation.clone());
|
.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 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
|
let vertical_layout = pos.x + MAX_MENU_WIDTH + POPOVER_GAP + MAX_MENU_WIDTH + POPOVER_GAP
|
||||||
> window.bounds().size.width;
|
> window.bounds().size.width;
|
||||||
|
|
@ -439,19 +427,7 @@ impl Render for CompletionMenu {
|
||||||
popover("completion-menu", cx)
|
popover("completion-menu", cx)
|
||||||
.w(MAX_MENU_WIDTH)
|
.w(MAX_MENU_WIDTH)
|
||||||
.px_2()
|
.px_2()
|
||||||
.child(
|
.child(render_markdown("doc", doc, window, cx)),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, deferred, div, px, App, AppContext as _, Bounds, Context, Empty, Entity,
|
prelude::FluentBuilder as _, px, App, AppContext as _, Bounds, Context, Empty, Entity,
|
||||||
InteractiveElement, IntoElement, ParentElement as _, Pixels, Point, Render, Styled, Window,
|
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 {
|
pub struct DiagnosticPopover {
|
||||||
state: Entity<InputState>,
|
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>) {
|
pub(crate) fn show(&mut self, cx: &mut Context<Self>) {
|
||||||
self.open = true;
|
self.open = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
@ -70,61 +64,32 @@ impl DiagnosticPopover {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for 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 {
|
if !self.open {
|
||||||
return Empty.into_any_element();
|
return Empty.into_any_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
let view = cx.entity();
|
|
||||||
|
|
||||||
let message = self.diagnostic.message.clone();
|
let message = self.diagnostic.message.clone();
|
||||||
let Some(pos) = self.origin(cx) else {
|
|
||||||
return Empty.into_any_element();
|
|
||||||
};
|
|
||||||
let (border, bg, fg) = (
|
let (border, bg, fg) = (
|
||||||
self.diagnostic.severity.border(cx),
|
self.diagnostic.severity.border(cx),
|
||||||
self.diagnostic.severity.bg(cx),
|
self.diagnostic.severity.bg(cx),
|
||||||
self.diagnostic.severity.fg(cx),
|
self.diagnostic.severity.fg(cx),
|
||||||
);
|
);
|
||||||
|
|
||||||
let scroll_origin = self.state.read(cx).scroll_handle.offset();
|
Popover::new(
|
||||||
|
"diagnostic-popover",
|
||||||
let y = pos.y - self.bounds.size.height + scroll_origin.y;
|
self.state.clone(),
|
||||||
let x = pos.x + scroll_origin.x;
|
self.diagnostic.range.clone(),
|
||||||
let max_width = px(500.).min(window.bounds().size.width - x);
|
move |window, cx| render_markdown("message", message.clone(), window, cx),
|
||||||
|
)
|
||||||
deferred(
|
.when(!self.open, |this| this.invisible())
|
||||||
div()
|
|
||||||
.id("diagnostic-popover")
|
|
||||||
.absolute()
|
|
||||||
.left(x)
|
|
||||||
.top(y)
|
|
||||||
.px_1()
|
.px_1()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.text_xs()
|
|
||||||
.max_w(max_width)
|
|
||||||
.bg(bg)
|
.bg(bg)
|
||||||
.text_color(fg)
|
.text_color(fg)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(border)
|
.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();
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
.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 code_action_menu;
|
||||||
mod completion_menu;
|
mod completion_menu;
|
||||||
mod diagnostic_popover;
|
mod diagnostic_popover;
|
||||||
|
mod hover_popover;
|
||||||
|
|
||||||
pub(crate) use code_action_menu::*;
|
pub(crate) use code_action_menu::*;
|
||||||
pub(crate) use completion_menu::*;
|
pub(crate) use completion_menu::*;
|
||||||
pub(crate) use diagnostic_popover::*;
|
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 {
|
pub(crate) enum ContextMenu {
|
||||||
Completion(Entity<CompletionMenu>),
|
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 rope::{Point, Rope};
|
||||||
|
|
||||||
use crate::input::Position;
|
use crate::input::Position;
|
||||||
|
|
@ -43,6 +45,12 @@ pub trait RopeExt {
|
||||||
|
|
||||||
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
/// Get the line, column [`Position`] (0-based) from the given byte offset.
|
||||||
fn offset_to_position(&self, offset: usize) -> Position;
|
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`.
|
/// An iterator over the lines of a `Rope`.
|
||||||
|
|
@ -152,8 +160,48 @@ impl RopeExt for Rope {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let offset = self.clip_offset(offset, sum_tree::Bias::Left);
|
||||||
self.slice(offset..self.len()).chars().next()
|
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)]
|
#[cfg(test)]
|
||||||
|
|
@ -267,4 +315,24 @@ mod tests {
|
||||||
assert_eq!(rope.char_at(38), Some('\n'));
|
assert_eq!(rope.char_at(38), Some('\n'));
|
||||||
assert_eq!(rope.char_at(50), None);
|
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,
|
text_wrapper::TextWrapper,
|
||||||
};
|
};
|
||||||
use crate::input::{
|
use crate::input::{
|
||||||
popovers::{ContextMenu, DiagnosticPopover},
|
popovers::{ContextMenu, DiagnosticPopover, HoverPopover},
|
||||||
search::{self, SearchPanel},
|
search::{self, SearchPanel},
|
||||||
Lsp, Position,
|
Lsp, Position,
|
||||||
};
|
};
|
||||||
|
|
@ -298,6 +298,7 @@ pub struct InputState {
|
||||||
pub(super) context_menu: Option<ContextMenu>,
|
pub(super) context_menu: Option<ContextMenu>,
|
||||||
/// A flag to indicate if we are currently inserting a completion item.
|
/// A flag to indicate if we are currently inserting a completion item.
|
||||||
pub(super) completion_inserting: bool,
|
pub(super) completion_inserting: bool,
|
||||||
|
pub(super) hover_popover: Option<Entity<HoverPopover>>,
|
||||||
|
|
||||||
pub lsp: Lsp,
|
pub lsp: Lsp,
|
||||||
|
|
||||||
|
|
@ -382,6 +383,7 @@ impl InputState {
|
||||||
diagnostic_popover: None,
|
diagnostic_popover: None,
|
||||||
context_menu: None,
|
context_menu: None,
|
||||||
completion_inserting: false,
|
completion_inserting: false,
|
||||||
|
hover_popover: None,
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
_context_menu_task: Task::ready(Ok(())),
|
_context_menu_task: Task::ready(Ok(())),
|
||||||
}
|
}
|
||||||
|
|
@ -599,7 +601,7 @@ impl InputState {
|
||||||
let local_offset = offset.saturating_sub(prev_lines_offset);
|
let local_offset = offset.saturating_sub(prev_lines_offset);
|
||||||
if let Some(pos) = line.position_for_index(local_offset, line_height) {
|
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 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));
|
return (line_index, sub_line_index, Some(adjusted_pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1573,9 +1575,11 @@ impl InputState {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.mode.is_code_editor() {
|
|
||||||
// Show diagnostic popover on mouse move
|
// Show diagnostic popover on mouse move
|
||||||
let offset = self.index_for_mouse_position(event.position, window, cx);
|
let offset = self.index_for_mouse_position(event.position, window, cx);
|
||||||
|
self.handle_hover(offset, window, cx);
|
||||||
|
|
||||||
|
if self.mode.is_code_editor() {
|
||||||
if let Some(diagnostic) = self
|
if let Some(diagnostic) = self
|
||||||
.mode
|
.mode
|
||||||
.diagnostics()
|
.diagnostics()
|
||||||
|
|
@ -2433,5 +2437,6 @@ impl Render for InputState {
|
||||||
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
|
.child(TextElement::new(cx.entity().clone()).placeholder(self.placeholder.clone()))
|
||||||
.children(self.diagnostic_popover.clone())
|
.children(self.diagnostic_popover.clone())
|
||||||
.children(self.context_menu.as_ref().map(|menu| menu.render()))
|
.children(self.context_menu.as_ref().map(|menu| menu.render()))
|
||||||
|
.children(self.hover_popover.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue