diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 07d41db38b2581e1866c6290d5fae0bc8cade8f3..8a893f8bc9a084ba55038d13c92c1ec6166fb983 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,5 +1,6 @@ use crate::{ hover_popover::{self, InlayHover}, + scroll::ScrollAmount, Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId, PointForPosition, SelectPhase, }; @@ -38,7 +39,11 @@ impl RangeInEditor { } } - fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + pub fn point_within_range( + &self, + trigger_point: &TriggerPoint, + snapshot: &EditorSnapshot, + ) -> bool { match (self, trigger_point) { (Self::Text(range), TriggerPoint::Text(point)) => { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); @@ -169,6 +174,21 @@ impl Editor { .detach(); } + pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext) -> bool { + let selection = self.selections.newest_anchor().head(); + let snapshot = self.snapshot(cx); + + let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| { + popover + .symbol_range + .point_within_range(&TriggerPoint::Text(selection), &snapshot) + }) else { + return false; + }; + popover.scroll(amount, cx); + true + } + fn cmd_click_reveal_task( &mut self, point: PointForPosition, diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index d3b732b986f195a045c9de9d5cdc5cad07808df4..6ce4afb17c2ed797b78382d9705e988b551a9e8e 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,14 +1,15 @@ use crate::{ display_map::{InlayOffset, ToDisplayPoint}, hover_links::{InlayHighlight, RangeInEditor}, + scroll::ScrollAmount, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, Hover, RangeToAnchorExt, }; use futures::{stream::FuturesUnordered, FutureExt}; use gpui::{ div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton, - ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task, - ViewContext, WeakView, + ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled, + Task, ViewContext, WeakView, }; use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; @@ -118,6 +119,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, + scroll_handle: ScrollHandle::new(), }; this.update(&mut cx, |this, cx| { @@ -317,6 +319,7 @@ fn show_hover( InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, + scroll_handle: ScrollHandle::new(), }, ) }) @@ -423,7 +426,7 @@ async fn parse_blocks( } } -#[derive(Default)] +#[derive(Default, Debug)] pub struct HoverState { pub info_popovers: Vec, pub diagnostic_popover: Option, @@ -487,10 +490,11 @@ impl HoverState { } } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct InfoPopover { - symbol_range: RangeInEditor, - parsed_content: ParsedMarkdown, + pub symbol_range: RangeInEditor, + pub parsed_content: ParsedMarkdown, + pub scroll_handle: ScrollHandle, } impl InfoPopover { @@ -504,23 +508,33 @@ impl InfoPopover { div() .id("info_popover") .elevation_2(cx) - .p_2() .overflow_y_scroll() + .track_scroll(&self.scroll_handle) .max_w(max_size.width) .max_h(max_size.height) // Prevent a mouse down/move on the popover from being propagated to the editor, // because that would dismiss the popover. .on_mouse_move(|_, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) - .child(crate::render_parsed_markdown( + .child(div().p_2().child(crate::render_parsed_markdown( "content", &self.parsed_content, style, workspace, cx, - )) + ))) .into_any_element() } + + pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext) { + let mut current = self.scroll_handle.offset(); + current.y -= amount.pixels( + cx.line_height(), + self.scroll_handle.bounds().size.height - px(16.), + ) / 2.0; + cx.notify(); + self.scroll_handle.set_offset(current); + } } #[derive(Debug, Clone)] diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 2cb22d15163323eae5f396e2415b973d099aae74..6f8cc1c2e5400831710720ac81c3ace8559e3582 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -1,7 +1,8 @@ use crate::Editor; use serde::Deserialize; +use ui::{px, Pixels}; -#[derive(Clone, PartialEq, Deserialize)] +#[derive(Debug, Clone, PartialEq, Deserialize)] pub enum ScrollAmount { // Scroll N lines (positive is towards the end of the document) Line(f32), @@ -25,4 +26,11 @@ impl ScrollAmount { .unwrap_or(0.), } } + + pub fn pixels(&self, line_height: Pixels, height: Pixels) -> Pixels { + match self { + ScrollAmount::Line(x) => px(line_height.0 * x), + ScrollAmount::Page(x) => px(height.0 * x), + } + } } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index e269dd6b2035aa6dc211abf1ab3732b10444e200..e80102166ee933002be3b6515c7cf9979b68efd7 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -2437,7 +2437,7 @@ where } } -#[derive(Default)] +#[derive(Default, Debug)] struct ScrollHandleState { offset: Rc>>, bounds: Bounds, @@ -2449,7 +2449,7 @@ struct ScrollHandleState { /// A handle to the scrollable aspects of an element. /// Used for accessing scroll state, like the current scroll offset, /// and for mutating the scroll state, like scrolling to a specific child. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ScrollHandle(Rc>); impl Default for ScrollHandle { @@ -2526,6 +2526,14 @@ impl ScrollHandle { } } + /// Set the offset explicitly. The offset is the distance from the top left of the + /// parent container to the top left of the first child. + /// As you scroll further down the offset becomes more negative. + pub fn set_offset(&self, mut position: Point) { + let state = self.0.borrow(); + *state.offset.borrow_mut() = position; + } + /// Get the logical scroll top, based on a child index and a pixel offset. pub fn logical_scroll_top(&self) -> (usize, Pixels) { let ix = self.top_item(); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 7a36caf213656c8664677f48bf89841d2afcceeb..79ac082673d0efaf1fea2abf04889f603c83e69c 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -69,6 +69,10 @@ fn scroll_editor( let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); let old_top_anchor = editor.scroll_manager.anchor().anchor; + if editor.scroll_hover(amount, cx) { + return; + } + editor.scroll_screen(amount, cx); if should_move_cursor { let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {