Pull hover popover out of context menu

Keith Simmons created

Change summary

crates/editor/src/editor.rs       | 50 +++++++++++++++-----------------
crates/editor/src/element.rs      | 40 ++++++++++++++++++++++++++
crates/project/src/lsp_command.rs | 37 +++++++++++++----------
crates/project/src/project.rs     |  8 ++++
crates/theme/src/theme.rs         |  3 +
styles/src/styleTree/editor.ts    |  2 +
6 files changed, 95 insertions(+), 45 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -424,7 +424,7 @@ pub struct Editor {
     next_completion_id: CompletionId,
     available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
     code_actions_task: Option<Task<()>>,
-    hover_task: Option<Task<()>>,
+    hover_task: Option<Task<Option<()>>>,
     document_highlights_task: Option<Task<()>>,
     pending_rename: Option<RenameState>,
     searchable: bool,
@@ -432,6 +432,7 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
     input_enabled: bool,
     leader_replica_id: Option<u16>,
+    hover_popover: Option<HoverPopover>,
 }
 
 pub struct EditorSnapshot {
@@ -571,7 +572,6 @@ struct InvalidationStack<T>(Vec<T>);
 enum ContextMenu {
     Completions(CompletionsMenu),
     CodeActions(CodeActionsMenu),
-    Hover(HoverPopover),
 }
 
 impl ContextMenu {
@@ -580,7 +580,6 @@ impl ContextMenu {
             match self {
                 ContextMenu::Completions(menu) => menu.select_prev(cx),
                 ContextMenu::CodeActions(menu) => menu.select_prev(cx),
-                _ => {}
             }
             true
         } else {
@@ -593,7 +592,6 @@ impl ContextMenu {
             match self {
                 ContextMenu::Completions(menu) => menu.select_next(cx),
                 ContextMenu::CodeActions(menu) => menu.select_next(cx),
-                _ => {}
             }
             true
         } else {
@@ -605,7 +603,6 @@ impl ContextMenu {
         match self {
             ContextMenu::Completions(menu) => menu.visible(),
             ContextMenu::CodeActions(menu) => menu.visible(),
-            ContextMenu::Hover(_) => true,
         }
     }
 
@@ -871,14 +868,16 @@ struct HoverPopover {
 }
 
 impl HoverPopover {
-    fn render(&self, style: EditorStyle) -> ElementBox {
-        let container_style = style.autocomplete.container;
-        Text::new(self.text.clone(), style.text.clone())
-            .with_soft_wrap(false)
-            .with_highlights(self.runs.clone())
-            .contained()
-            .with_style(container_style)
-            .boxed()
+    fn render(&self, style: EditorStyle) -> (DisplayPoint, ElementBox) {
+        (
+            self.point,
+            Text::new(self.text.clone(), style.text.clone())
+                .with_soft_wrap(false)
+                .with_highlights(self.runs.clone())
+                .contained()
+                .with_style(style.hover_popover)
+                .boxed(),
+        )
     }
 }
 
@@ -1048,6 +1047,7 @@ impl Editor {
             keymap_context_layers: Default::default(),
             input_enabled: true,
             leader_replica_id: None,
+            hover_popover: None,
         };
         this.end_selection(cx);
 
@@ -1433,6 +1433,8 @@ impl Editor {
                 }
             }
 
+            self.hover_popover.take();
+
             if old_cursor_position.to_display_point(&display_map).row()
                 != new_cursor_position.to_display_point(&display_map).row()
             {
@@ -2456,7 +2458,6 @@ impl Editor {
 
         let point = action.0.clone();
 
-        let id = post_inc(&mut self.next_completion_id);
         let task = cx.spawn_weak(|this, mut cx| {
             async move {
                 // TODO: what to show while language server is loading?
@@ -2475,7 +2476,7 @@ impl Editor {
                     },
                 };
 
-                let mut hover_popover = HoverPopover {
+                let hover_popover = HoverPopover {
                     // TODO: fix tooltip to beginning of symbol based on range
                     point,
                     text,
@@ -2484,15 +2485,8 @@ impl Editor {
 
                 if let Some(this) = this.upgrade(&cx) {
                     this.update(&mut cx, |this, cx| {
-                        if !matches!(
-                            this.context_menu.as_ref(),
-                            None | Some(ContextMenu::Hover(_))
-                        ) {
-                            return;
-                        }
-
                         if this.focused {
-                            this.show_context_menu(ContextMenu::Hover(hover_popover), cx);
+                            this.hover_popover = Some(hover_popover);
                         }
 
                         cx.notify();
@@ -2502,7 +2496,8 @@ impl Editor {
             }
             .log_err()
         });
-        self.completion_tasks.push((id, task));
+
+        self.hover_task = Some(task);
     }
 
     async fn open_project_transaction(
@@ -2751,6 +2746,10 @@ impl Editor {
             .map(|menu| menu.render(cursor_position, style))
     }
 
+    pub fn render_hover_popover(&self, style: EditorStyle) -> Option<(DisplayPoint, ElementBox)> {
+        self.hover_popover.as_ref().map(|hover| hover.render(style))
+    }
+
     fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {
         if !matches!(menu, ContextMenu::Completions(_)) {
             self.completion_tasks.clear();
@@ -5785,9 +5784,6 @@ impl View for Editor {
             Some(ContextMenu::CodeActions(_)) => {
                 context.set.insert("showing_code_actions".into());
             }
-            Some(ContextMenu::Hover(_)) => {
-                context.set.insert("showing_hover".into());
-            }
             None => {}
         }
 

crates/editor/src/element.rs 🔗

@@ -508,6 +508,28 @@ impl EditorElement {
             cx.scene.pop_stacking_context();
         }
 
+        if let Some((position, hover_popover)) = layout.hover.as_mut() {
+            cx.scene.push_stacking_context(None);
+
+            let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
+            let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
+            let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
+            let mut popover_origin = content_origin + vec2f(x, y);
+            let popover_height = hover_popover.size().y();
+
+            if popover_origin.y() + popover_height > bounds.lower_left().y() {
+                popover_origin.set_y(popover_origin.y() - layout.line_height - popover_height);
+            }
+
+            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();
     }
 
@@ -1081,6 +1103,7 @@ 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
@@ -1097,6 +1120,8 @@ impl Element for EditorElement {
                 code_actions_indicator = view
                     .render_code_actions_indicator(&style, cx)
                     .map(|indicator| (newest_selection_head.row(), indicator));
+
+                hover = view.render_hover_popover(style);
             }
         });
 
@@ -1120,6 +1145,19 @@ impl Element for EditorElement {
             );
         }
 
+        if let Some((_, hover)) = hover.as_mut() {
+            hover.layout(
+                SizeConstraint {
+                    min: Vector2F::zero(),
+                    max: vec2f(
+                        f32::INFINITY,
+                        (12. * line_height).min((size.y() - line_height) / 2.),
+                    ),
+                },
+                cx,
+            );
+        }
+
         let blocks = self.layout_blocks(
             start_row..end_row,
             &snapshot,
@@ -1156,6 +1194,7 @@ impl Element for EditorElement {
                 selections,
                 context_menu,
                 code_actions_indicator,
+                hover,
             },
         )
     }
@@ -1299,6 +1338,7 @@ pub struct LayoutState {
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
+    hover: Option<(DisplayPoint, ElementBox)>,
 }
 
 fn layout_line(

crates/project/src/lsp_command.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{DocumentHighlight, Location, Project, ProjectTransaction};
+use crate::{DocumentHighlight, Location, Project, ProjectTransaction, Hover};
 use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use client::{proto, PeerId};
@@ -801,8 +801,7 @@ impl LspCommand for GetDocumentHighlights {
 
 #[async_trait(?Send)]
 impl LspCommand for GetHover {
-    // TODO: proper response type
-    type Response = Option<lsp::Hover>;
+    type Response = Option<Hover>;
     type LspRequest = lsp::request::HoverRequest;
     type ProtoRequest = proto::GetHover;
 
@@ -823,20 +822,26 @@ impl LspCommand for GetHover {
         message: Option<lsp::Hover>,
         project: ModelHandle<Project>,
         buffer: ModelHandle<Buffer>,
-        cx: AsyncAppContext,
+        mut cx: AsyncAppContext,
     ) -> Result<Self::Response> {
-        // let (lsp_adapter, language_server) = project
-        //     .read_with(&cx, |project, cx| {
-        //         project
-        //             .language_server_for_buffer(buffer.read(cx), cx)
-        //             .cloned()
-        //     })
-        //     .ok_or_else(|| anyhow!("no language server found for buffer"))?;
-
-        // TODO: what here?
-        Ok(Some(
-            message.ok_or_else(|| anyhow!("invalid lsp response"))?,
-        ))
+        Ok(message.map(|hover| {
+            let range = hover.range.map(|range| {
+                cx.read(|cx| {
+                    let buffer = buffer.read(cx);
+                    let token_start = buffer
+                        .clip_point_utf16(point_from_lsp(range.start), Bias::Left);
+                    let token_end = buffer
+                        .clip_point_utf16(point_from_lsp(range.end), Bias::Left);
+                    buffer.anchor_after(token_start)..
+                        buffer.anchor_before(token_end)
+                })
+            });
+            
+            Hover {
+                contents: hover.contents,
+                range
+            }
+        }))
     }
 
     fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {

crates/project/src/project.rs 🔗

@@ -216,6 +216,12 @@ pub struct Symbol {
     pub signature: [u8; 32],
 }
 
+#[derive(Debug)]
+pub struct Hover {
+    pub contents: lsp::HoverContents,
+    pub range: Option<Range<language::Anchor>>,
+}
+
 #[derive(Default)]
 pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
 
@@ -2890,7 +2896,7 @@ impl Project {
         buffer: &ModelHandle<Buffer>,
         position: T,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<Option<lsp::Hover>>> {
+    ) -> Task<Result<Option<Hover>>> {
         // TODO: proper return type
         let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(buffer.clone(), GetHover { position }, cx)

crates/theme/src/theme.rs 🔗

@@ -444,6 +444,7 @@ pub struct Editor {
     pub autocomplete: AutocompleteStyle,
     pub code_actions_indicator: Color,
     pub unnecessary_code_fade: f32,
+    pub hover_popover: ContainerStyle,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -621,4 +622,4 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
 
         Ok(result)
     }
-}
+}

styles/src/styleTree/editor.ts 🔗

@@ -8,6 +8,7 @@ import {
   text,
   TextColor
 } from "./components";
+import hoverPopover from "./hoverPopover";
 
 export default function editor(theme: Theme) {
   const autocompleteItem = {
@@ -145,6 +146,7 @@ export default function editor(theme: Theme) {
     invalidHintDiagnostic: diagnostic(theme, "muted"),
     invalidInformationDiagnostic: diagnostic(theme, "muted"),
     invalidWarningDiagnostic: diagnostic(theme, "muted"),
+    hover_popover: hoverPopover(theme),
     syntax,
   };
 }