Add vim bindings for hover
Allow scrolling in hover popover
This commit is contained in:
parent
67d9abc00f
commit
a6c0ee472c
11 changed files with 1058 additions and 1006 deletions
|
@ -25,9 +25,9 @@ use gpui::{
|
|||
geometry::vector::{vec2f, Vector2F},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::CursorStyle,
|
||||
text_layout, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, ElementBox, Entity,
|
||||
ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
text_layout, AppContext, AsyncAppContext, Axis, ClipboardItem, Element, ElementBox,
|
||||
ElementStateContext, Entity, ModelHandle, MutableAppContext, ReadModel, RenderContext, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
|
@ -81,7 +81,7 @@ pub struct Scroll(pub Vector2F);
|
|||
pub struct Select(pub SelectPhase);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Hover {
|
||||
pub struct HoverAt {
|
||||
point: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
|
@ -198,6 +198,7 @@ actions!(
|
|||
ShowCompletions,
|
||||
OpenExcerpts,
|
||||
RestartLanguageServer,
|
||||
Hover,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -214,7 +215,7 @@ impl_actions!(
|
|||
]
|
||||
);
|
||||
|
||||
impl_internal_actions!(editor, [Scroll, Select, Hover, GoToDefinitionAt]);
|
||||
impl_internal_actions!(editor, [Scroll, Select, HoverAt, GoToDefinitionAt]);
|
||||
|
||||
enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
|
@ -302,6 +303,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(Editor::show_completions);
|
||||
cx.add_action(Editor::toggle_code_actions);
|
||||
cx.add_action(Editor::hover);
|
||||
cx.add_action(Editor::hover_at);
|
||||
cx.add_action(Editor::open_excerpts);
|
||||
cx.add_action(Editor::restart_language_server);
|
||||
cx.add_async_action(Editor::confirm_completion);
|
||||
|
@ -888,48 +890,64 @@ impl CodeActionsMenu {
|
|||
}
|
||||
}
|
||||
|
||||
struct HoverPopover {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HoverPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub hover_point: DisplayPoint,
|
||||
pub range: Range<DisplayPoint>,
|
||||
pub contents: Vec<HoverBlock>,
|
||||
}
|
||||
|
||||
impl HoverPopover {
|
||||
fn render(&self, style: EditorStyle, project: &Project) -> (DisplayPoint, ElementBox) {
|
||||
let mut flex = Flex::new(Axis::Vertical);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
if let Some(language) = content
|
||||
.language
|
||||
.clone()
|
||||
.and_then(|language| project.languages().get_language(&language))
|
||||
{
|
||||
let runs =
|
||||
language.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||
fn render<C: ElementStateContext + ReadModel>(
|
||||
&self,
|
||||
style: EditorStyle,
|
||||
cx: &mut C,
|
||||
) -> (DisplayPoint, ElementBox) {
|
||||
let element = MouseEventHandler::new::<HoverPopover, _, _>(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(language) = content
|
||||
.language
|
||||
.clone()
|
||||
.and_then(|language| project.languages().get_language(&language))
|
||||
{
|
||||
let runs = language
|
||||
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||
|
||||
Text::new(content.text.clone(), style.text.clone())
|
||||
.with_soft_wrap(true)
|
||||
.with_highlights(
|
||||
runs.iter()
|
||||
.filter_map(|(range, id)| {
|
||||
id.style(style.theme.syntax.as_ref())
|
||||
.map(|style| (range.clone(), style))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
Text::new(content.text.clone(), style.hover_popover.prose.clone())
|
||||
.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(style.hover_popover.block_style)
|
||||
.boxed()
|
||||
}
|
||||
}));
|
||||
(
|
||||
self.range.start,
|
||||
Text::new(content.text.clone(), style.text.clone())
|
||||
.with_soft_wrap(true)
|
||||
.with_highlights(
|
||||
runs.iter()
|
||||
.filter_map(|(range, id)| {
|
||||
id.style(style.theme.syntax.as_ref())
|
||||
.map(|style| (range.clone(), style))
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
Text::new(content.text.clone(), style.hover_popover.prose.clone())
|
||||
.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(style.hover_popover.block_style)
|
||||
.boxed()
|
||||
}
|
||||
}));
|
||||
flex.contained()
|
||||
.with_style(style.hover_popover.container)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.with_padding(Padding {
|
||||
bottom: 5.,
|
||||
top: 5.,
|
||||
..Default::default()
|
||||
})
|
||||
.boxed();
|
||||
|
||||
(self.range.start, element)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1489,7 +1507,7 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
self.hover_state.close();
|
||||
self.hide_hover(cx);
|
||||
|
||||
if old_cursor_position.to_display_point(&display_map).row()
|
||||
!= new_cursor_position.to_display_point(&display_map).row()
|
||||
|
@ -1852,6 +1870,10 @@ impl Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
if self.hide_hover(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.hide_context_menu(cx).is_some() {
|
||||
return;
|
||||
}
|
||||
|
@ -2480,49 +2502,65 @@ impl Editor {
|
|||
}))
|
||||
}
|
||||
|
||||
/// The hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
fn hover(&mut self, _: &Hover, cx: &mut ViewContext<Self>) {
|
||||
let head = self.selections.newest_display(cx).head();
|
||||
self.show_hover(head, true, cx);
|
||||
}
|
||||
|
||||
/// The internal hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// depending on whether a point to hover over is provided.
|
||||
fn hover(&mut self, action: &Hover, cx: &mut ViewContext<Self>) {
|
||||
fn hover_at(&mut self, action: &HoverAt, cx: &mut ViewContext<Self>) {
|
||||
if let Some(point) = action.point {
|
||||
self.show_hover(point, cx);
|
||||
self.show_hover(point, false, cx);
|
||||
} else {
|
||||
self.hide_hover(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hides the type information popup ASAP.
|
||||
/// Triggered by the `Hover` action when the cursor is not over a symbol.
|
||||
fn hide_hover(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
async move {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// consistently keep track of state to make handoff smooth
|
||||
let (_recent_hover, _in_grace) = this.hover_state.determine_state(false);
|
||||
/// Hides the type information popup.
|
||||
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
|
||||
/// selecitons changed.
|
||||
fn hide_hover(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
// consistently keep track of state to make handoff smooth
|
||||
self.hover_state.determine_state(false);
|
||||
|
||||
// only notify the context once
|
||||
if this.hover_state.popover.is_some() {
|
||||
this.hover_state.popover = None;
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
let mut did_hide = false;
|
||||
|
||||
self.hover_task = Some(task);
|
||||
// only notify the context once
|
||||
if self.hover_state.popover.is_some() {
|
||||
self.hover_state.popover = None;
|
||||
did_hide = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
self.clear_background_highlights::<HoverState>(cx);
|
||||
|
||||
self.hover_task = None;
|
||||
|
||||
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(&mut self, point: DisplayPoint, cx: &mut ViewContext<Self>) {
|
||||
fn show_hover(
|
||||
&mut self,
|
||||
point: DisplayPoint,
|
||||
ignore_timeout: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(hover) = &self.hover_state.popover {
|
||||
if hover.hover_point == point {
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let snapshot = self.snapshot(cx);
|
||||
let (buffer, buffer_position) = if let Some(output) = self
|
||||
.buffer
|
||||
|
@ -2534,15 +2572,6 @@ impl Editor {
|
|||
return;
|
||||
};
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
if let Some(existing_popover) = &self.hover_state.popover {
|
||||
if existing_popover.range.contains(&point) {
|
||||
// Hover already contains value. No need to request a new one
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let project = if let Some(project) = self.project.clone() {
|
||||
project
|
||||
} else {
|
||||
|
@ -2550,49 +2579,44 @@ impl Editor {
|
|||
};
|
||||
|
||||
// query the LSP for hover info
|
||||
let hover = project.update(cx, |project, cx| {
|
||||
let hover_request = project.update(cx, |project, cx| {
|
||||
project.hover(&buffer, buffer_position.clone(), cx)
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
async move {
|
||||
let mut contents = None;
|
||||
|
||||
let hover = match hover.await {
|
||||
Ok(hover) => hover,
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let mut symbol_range = point..point;
|
||||
|
||||
// determine the contents of the popover
|
||||
if let Some(hover) = hover {
|
||||
if hover.contents.is_empty() {
|
||||
contents = None;
|
||||
} else {
|
||||
contents = Some(hover.contents);
|
||||
|
||||
if let Some(range) = hover.range {
|
||||
let offset_range = range.to_offset(&buffer_snapshot);
|
||||
if offset_range
|
||||
.contains(&point.to_offset(&snapshot.display_snapshot, Bias::Left))
|
||||
{
|
||||
symbol_range = offset_range
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
..offset_range
|
||||
.end
|
||||
.to_display_point(&snapshot.display_snapshot);
|
||||
} else {
|
||||
contents = None;
|
||||
}
|
||||
}
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
if hover_result.contents.is_empty() {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let hover_popover = contents.map(|contents| HoverPopover {
|
||||
range: symbol_range,
|
||||
contents,
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let offset_range = range.to_offset(&buffer_snapshot);
|
||||
if !offset_range
|
||||
.contains(&point.to_offset(&snapshot.display_snapshot, Bias::Left))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
offset_range
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
..offset_range
|
||||
.end
|
||||
.to_display_point(&snapshot.display_snapshot)
|
||||
} else {
|
||||
point..point
|
||||
};
|
||||
|
||||
Some(HoverPopover {
|
||||
project: project.clone(),
|
||||
hover_point: point,
|
||||
range,
|
||||
contents: hover_result.contents,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
|
@ -2612,7 +2636,32 @@ impl Editor {
|
|||
|
||||
// `smooth_handoff` and `in_grace` determine whether to switch right away.
|
||||
// `recent_hover` will activate the handoff after the initial delay.
|
||||
if (smooth_handoff || !recent_hover || in_grace) && visible {
|
||||
// `ignore_timeout` is set when the user manually sent the hover action.
|
||||
if (ignore_timeout || smooth_handoff || !recent_hover || in_grace)
|
||||
&& visible
|
||||
{
|
||||
// Highlight the selected symbol using a background highlight
|
||||
if let Some(display_range) =
|
||||
hover_popover.as_ref().map(|popover| popover.range.clone())
|
||||
{
|
||||
let start = snapshot.display_snapshot.buffer_snapshot.anchor_after(
|
||||
display_range
|
||||
.start
|
||||
.to_offset(&snapshot.display_snapshot, Bias::Right),
|
||||
);
|
||||
let end = snapshot.display_snapshot.buffer_snapshot.anchor_before(
|
||||
display_range
|
||||
.end
|
||||
.to_offset(&snapshot.display_snapshot, Bias::Left),
|
||||
);
|
||||
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![start..end],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
this.hover_state.popover = hover_popover;
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -2869,18 +2918,11 @@ impl Editor {
|
|||
) -> Option<(DisplayPoint, ElementBox)> {
|
||||
self.context_menu
|
||||
.as_ref()
|
||||
.map(|menu| menu.render(cursor_position, style))
|
||||
.map(|menu| menu.render(cursor_position, style, cx))
|
||||
}
|
||||
|
||||
pub fn render_hover_popover(
|
||||
&self,
|
||||
style: EditorStyle,
|
||||
project: &Project,
|
||||
) -> Option<(DisplayPoint, ElementBox)> {
|
||||
self.hover_state
|
||||
.popover
|
||||
.as_ref()
|
||||
.map(|hover| hover.render(style, project))
|
||||
pub(crate) fn hover_popover(&self) -> Option<HoverPopover> {
|
||||
self.hover_state.popover.clone()
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
|
||||
|
@ -4963,7 +5005,6 @@ impl Editor {
|
|||
// Position the selection in the rename editor so that it matches the current selection.
|
||||
this.show_local_selections = false;
|
||||
let rename_editor = cx.add_view(|cx| {
|
||||
println!("Rename editor created.");
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
if let Some(old_highlight_id) = old_highlight_id {
|
||||
editor.override_text_style =
|
||||
|
|
|
@ -5,7 +5,7 @@ use super::{
|
|||
};
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, TransformBlock},
|
||||
EditorStyle, GoToDefinition, Hover,
|
||||
EditorStyle, GoToDefinition, HoverAt,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
|
@ -509,36 +509,31 @@ impl EditorElement {
|
|||
}
|
||||
|
||||
if let Some((position, hover_popover)) = layout.hover.as_mut() {
|
||||
if position.row() >= start_row {
|
||||
if let Some(cursor_row_layout) = &layout
|
||||
.line_layouts
|
||||
.get((position.row() - start_row) as usize)
|
||||
{
|
||||
cx.scene.push_stacking_context(None);
|
||||
cx.scene.push_stacking_context(None);
|
||||
|
||||
let size = hover_popover.size();
|
||||
let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
let y = position.row() as f32 * layout.line_height - scroll_top - size.y();
|
||||
let mut popover_origin = content_origin + vec2f(x, y);
|
||||
// This is safe because we check on layout whether the required row is available
|
||||
let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
|
||||
let size = hover_popover.size();
|
||||
let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
|
||||
let y = position.row() as f32 * layout.line_height - scroll_top - size.y();
|
||||
let mut popover_origin = content_origin + vec2f(x, y);
|
||||
|
||||
if popover_origin.y() < 0.0 {
|
||||
popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
|
||||
}
|
||||
|
||||
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
|
||||
if x_out_of_bounds < 0.0 {
|
||||
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
|
||||
}
|
||||
|
||||
hover_popover.paint(
|
||||
popover_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
if popover_origin.y() < 0.0 {
|
||||
popover_origin.set_y(popover_origin.y() + layout.line_height + size.y());
|
||||
}
|
||||
|
||||
let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
|
||||
if x_out_of_bounds < 0.0 {
|
||||
popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
|
||||
}
|
||||
|
||||
hover_popover.paint(
|
||||
popover_origin,
|
||||
RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.scene.pop_stacking_context();
|
||||
}
|
||||
|
||||
cx.scene.pop_layer();
|
||||
|
@ -1114,7 +1109,6 @@ impl Element for EditorElement {
|
|||
|
||||
let mut context_menu = None;
|
||||
let mut code_actions_indicator = None;
|
||||
let mut hover = None;
|
||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let newest_selection_head = view
|
||||
.selections
|
||||
|
@ -1125,18 +1119,14 @@ impl Element for EditorElement {
|
|||
let style = view.style(cx);
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
if view.context_menu_visible() {
|
||||
context_menu = view.render_context_menu(newest_selection_head, style.clone());
|
||||
context_menu =
|
||||
view.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
}
|
||||
|
||||
code_actions_indicator = view
|
||||
.render_code_actions_indicator(&style, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
if let Some(project) = view.project.clone() {
|
||||
let project = project.read(cx);
|
||||
hover = view.render_hover_popover(style, project);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
|
@ -1159,18 +1149,27 @@ impl Element for EditorElement {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some((_, hover)) = hover.as_mut() {
|
||||
hover.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
(120. * em_width).min(size.x()),
|
||||
(size.y() - line_height) * 3. / 2.,
|
||||
),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
let hover = self.view(cx).hover_popover().and_then(|hover| {
|
||||
let (point, mut rendered) = hover.render(style.clone(), cx);
|
||||
|
||||
if point.row() >= snapshot.scroll_position().y() as u32 {
|
||||
if line_layouts.len() > (point.row() - start_row) as usize {
|
||||
rendered.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
max: vec2f(
|
||||
(120. * em_width).min(size.x()),
|
||||
(size.y() - line_height) * 1. / 2.,
|
||||
),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
return Some((point, rendered));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
|
||||
let blocks = self.layout_blocks(
|
||||
start_row..end_row,
|
||||
|
@ -1270,6 +1269,12 @@ impl Element for EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some((_, hover)) = &mut layout.hover {
|
||||
if hover.dispatch_event(event, cx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (_, block) in &mut layout.blocks {
|
||||
if block.dispatch_event(event, cx) {
|
||||
return true;
|
||||
|
@ -1317,7 +1322,7 @@ impl Element for EditorElement {
|
|||
None
|
||||
};
|
||||
|
||||
cx.dispatch_action(Hover { point });
|
||||
cx.dispatch_action(HoverAt { point });
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
|
|
|
@ -195,6 +195,14 @@ impl SelectionsCollection {
|
|||
resolve(self.newest_anchor(), &self.buffer(cx))
|
||||
}
|
||||
|
||||
pub fn newest_display(&self, cx: &mut MutableAppContext) -> Selection<DisplayPoint> {
|
||||
let display_map = self.display_map(cx);
|
||||
let selection = self
|
||||
.newest_anchor()
|
||||
.map(|point| point.to_display_point(&display_map));
|
||||
selection
|
||||
}
|
||||
|
||||
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
|
||||
self.disjoint
|
||||
.iter()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue