Added click regions and cursor styles

Mikayla Maki created

Change summary

crates/editor/src/display_map/fold_map.rs |   2 
crates/editor/src/editor.rs               | 162 +++++++++++++++++++++---
crates/editor/src/element.rs              | 148 ++++++++++++++++++++--
3 files changed, 274 insertions(+), 38 deletions(-)

Detailed changes

crates/editor/src/display_map/fold_map.rs 🔗

@@ -370,7 +370,7 @@ impl FoldMap {
                     }
 
                     if fold.end > fold.start {
-                        let output_text = "…";
+                        let output_text = "⋯";
                         new_transforms.push(
                             Transform {
                                 summary: TransformSummary {

crates/editor/src/editor.rs 🔗

@@ -39,10 +39,11 @@ use gpui::{
     impl_actions, impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::CursorStyle,
+    scene::MouseClick,
     serde_json::json,
     AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
-    ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    EventContext, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HideHover, HoverState};
@@ -457,6 +458,8 @@ type CompletionId = usize;
 
 type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
+type TextClickedCallback =
+    fn(&MouseClick, &Range<DisplayPoint>, &EditorSnapshot, &mut EventContext);
 
 pub struct Editor {
     handle: WeakViewHandle<Self>,
@@ -485,6 +488,7 @@ pub struct Editor {
     highlighted_rows: Option<Range<u32>>,
     #[allow(clippy::type_complexity)]
     background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
+    clickable_text: BTreeMap<TypeId, (TextClickedCallback, Vec<Range<Anchor>>)>,
     nav_history: Option<ItemNavHistory>,
     context_menu: Option<ContextMenu>,
     mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
@@ -1155,6 +1159,7 @@ impl Editor {
             placeholder_text: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
+            clickable_text: Default::default(),
             nav_history: None,
             context_menu: None,
             mouse_context_menu: cx.add_view(context_menu::ContextMenu::new),
@@ -5776,15 +5781,11 @@ impl Editor {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
         if let Some(fold_range) = display_map.foldable_range(display_row) {
-            let autoscroll = {
-                let selections = self.selections.all::<Point>(cx);
-
-                selections.iter().any(|selection| {
-                    let display_range = selection.display_range(&display_map);
-
-                    fold_range.overlaps(&display_range)
-                })
-            };
+            let autoscroll = self
+                .selections
+                .all::<Point>(cx)
+                .iter()
+                .any(|selection| fold_range.overlaps(&selection.display_range(&display_map)));
 
             let fold_range =
                 fold_range.start.to_point(&display_map)..fold_range.end.to_point(&display_map);
@@ -5816,14 +5817,11 @@ impl Editor {
 
         let unfold_range = fold_at.display_row.to_points(&display_map);
 
-        let autoscroll = {
-            let selections = self.selections.all::<Point>(cx);
-            selections.iter().any(|selection| {
-                let display_range = selection.display_range(&display_map);
-
-                unfold_range.overlaps(&display_range)
-            })
-        };
+        let autoscroll = self
+            .selections
+            .all::<Point>(cx)
+            .iter()
+            .any(|selection| unfold_range.overlaps(&selection.display_range(&display_map)));
 
         let unfold_range =
             unfold_range.start.to_point(&display_map)..unfold_range.end.to_point(&display_map);
@@ -5836,7 +5834,7 @@ impl Editor {
         self.fold_ranges(ranges, true, cx);
     }
 
-    pub fn fold_ranges<T: ToOffset>(
+    pub fn fold_ranges<T: ToOffset + Clone>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,
         auto_scroll: bool,
@@ -5844,15 +5842,33 @@ impl Editor {
     ) {
         let mut ranges = ranges.into_iter().peekable();
         if ranges.peek().is_some() {
-            self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
+            let ranges = ranges.collect_vec();
+
+            self.display_map
+                .update(cx, |map, cx| map.fold(ranges.iter().cloned(), cx));
+
             if auto_scroll {
                 self.request_autoscroll(Autoscroll::fit(), cx);
             }
+
+            let snapshot = self.snapshot(cx);
+            let anchor_ranges = offset_to_anchors(ranges, &snapshot);
+
+            self.change_click_ranges::<FoldMarker>(cx, |click_ranges| {
+                for range in anchor_ranges {
+                    if let Err(idx) = click_ranges.binary_search_by(|click_range| {
+                        click_range.cmp(&range, &snapshot.buffer_snapshot)
+                    }) {
+                        click_ranges.insert(idx, range)
+                    }
+                }
+            });
+
             cx.notify();
         }
     }
 
-    pub fn unfold_ranges<T: ToOffset>(
+    pub fn unfold_ranges<T: ToOffset + Clone>(
         &mut self,
         ranges: impl IntoIterator<Item = Range<T>>,
         inclusive: bool,
@@ -5861,11 +5877,35 @@ impl Editor {
     ) {
         let mut ranges = ranges.into_iter().peekable();
         if ranges.peek().is_some() {
-            self.display_map
-                .update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
+            let ranges = ranges.collect_vec();
+
+            self.display_map.update(cx, |map, cx| {
+                map.unfold(ranges.iter().cloned(), inclusive, cx)
+            });
             if auto_scroll {
                 self.request_autoscroll(Autoscroll::fit(), cx);
             }
+
+            let snapshot = self.snapshot(cx);
+            let anchor_ranges = offset_to_anchors(ranges, &snapshot);
+
+            self.change_click_ranges::<FoldMarker>(cx, |click_ranges| {
+                for range in anchor_ranges {
+                    let range_point = range.start.to_point(&snapshot.buffer_snapshot);
+                    // Fold and unfold ranges start at different points in the row.
+                    // But their rows do match, so we can use that to detect sameness.
+                    if let Ok(idx) = click_ranges.binary_search_by(|click_range| {
+                        click_range
+                            .start
+                            .to_point(&snapshot.buffer_snapshot)
+                            .row
+                            .cmp(&range_point.row)
+                    }) {
+                        click_ranges.remove(idx);
+                    }
+                }
+            });
+
             cx.notify();
         }
     }
@@ -5991,6 +6031,61 @@ impl Editor {
         }
     }
 
+    pub fn change_click_ranges<T: ClickRange>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        change: impl FnOnce(&mut Vec<Range<Anchor>>),
+    ) {
+        let mut ranges = self
+            .clickable_text
+            .remove(&TypeId::of::<T>())
+            .map(|click_range| click_range.1)
+            .unwrap_or_default();
+
+        change(&mut ranges);
+
+        self.clickable_text
+            .insert(TypeId::of::<T>(), (T::click_handler, ranges));
+
+        cx.notify();
+    }
+
+    pub fn click_ranges_in_range(
+        &self,
+        search_range: Range<Anchor>,
+        display_snapshot: &DisplaySnapshot,
+    ) -> Vec<(Range<DisplayPoint>, TextClickedCallback)> {
+        let mut results = Vec::new();
+        let buffer = &display_snapshot.buffer_snapshot;
+        for (callback, ranges) in self.clickable_text.values() {
+            let start_ix = match ranges.binary_search_by(|probe| {
+                let cmp = probe.end.cmp(&search_range.start, buffer);
+                if cmp.is_gt() {
+                    Ordering::Greater
+                } else {
+                    Ordering::Less
+                }
+            }) {
+                Ok(i) | Err(i) => i,
+            };
+            for range in &ranges[start_ix..] {
+                if range.start.cmp(&search_range.end, buffer).is_ge() {
+                    break;
+                }
+                let start = range
+                    .start
+                    .to_point(buffer)
+                    .to_display_point(display_snapshot);
+                let end = range
+                    .end
+                    .to_point(buffer)
+                    .to_display_point(display_snapshot);
+                results.push((start..end, *callback))
+            }
+        }
+        results
+    }
+
     pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
         self.highlighted_rows = rows;
     }
@@ -6369,6 +6464,25 @@ fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot)
     }
 }
 
+fn offset_to_anchors<
+    'snapshot,
+    'iter: 'snapshot,
+    T: ToOffset,
+    I: IntoIterator<Item = Range<T>> + 'iter,
+>(
+    ranges: I,
+    snapshot: &'snapshot EditorSnapshot,
+) -> impl Iterator<Item = Range<Anchor>> + 'snapshot {
+    ranges.into_iter().map(|range| {
+        snapshot
+            .buffer_snapshot
+            .anchor_at(range.start.to_offset(&snapshot.buffer_snapshot), Bias::Left)
+            ..snapshot
+                .buffer_snapshot
+                .anchor_at(range.end.to_offset(&snapshot.buffer_snapshot), Bias::Right)
+    })
+}
+
 impl EditorSnapshot {
     pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
         self.display_snapshot.buffer_snapshot.language_at(position)

crates/editor/src/element.rs 🔗

@@ -4,7 +4,7 @@ use super::{
     ToPoint, MAX_LINE_LEN,
 };
 use crate::{
-    display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
+    display_map::{BlockStyle, DisplayRow, DisplaySnapshot, FoldStatus, TransformBlock},
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
         HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
@@ -14,7 +14,7 @@ use crate::{
     },
     mouse_context_menu::DeployMouseContextMenu,
     scroll::actions::Scroll,
-    EditorStyle, GutterHover,
+    EditorStyle, GutterHover, TextClickedCallback, UnfoldAt,
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
@@ -30,6 +30,7 @@ use gpui::{
     },
     json::{self, ToJson},
     platform::CursorStyle,
+    scene::MouseClick,
     text_layout::{self, Line, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, CursorRegion, Element, ElementBox, EventContext, LayoutContext,
     Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent, MouseRegion, MutableAppContext,
@@ -115,6 +116,7 @@ impl EditorElement {
     fn attach_mouse_handlers(
         view: &WeakViewHandle<Editor>,
         position_map: &Arc<PositionMap>,
+        click_ranges: Arc<Vec<(Range<DisplayPoint>, TextClickedCallback)>>,
         has_popovers: bool,
         visible_bounds: RectF,
         text_bounds: RectF,
@@ -211,6 +213,20 @@ impl EditorElement {
                             cx.propagate_event()
                         }
                     }
+                })
+                .on_click(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        let point =
+                            position_to_display_point(e.position, text_bounds, &position_map);
+                        if let Some(point) = point {
+                            for (range, callback) in click_ranges.iter() {
+                                if range.contains(&point) {
+                                    callback(&e, range, &position_map.snapshot, cx)
+                                }
+                            }
+                        }
+                    }
                 }),
         );
 
@@ -412,16 +428,7 @@ impl EditorElement {
     ) -> bool {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
         // Don't trigger hover popover if mouse is hovering over context menu
-        let point = if text_bounds.contains_point(position) {
-            let (point, target_point) = position_map.point_for_position(text_bounds, position);
-            if point == target_point {
-                Some(point)
-            } else {
-                None
-            }
-        } else {
-            None
-        };
+        let point = position_to_display_point(position, text_bounds, position_map);
 
         cx.dispatch_action(UpdateGoToDefinitionLink {
             point,
@@ -702,6 +709,7 @@ impl EditorElement {
         let max_glyph_width = layout.position_map.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
         let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
+        let line_end_overshoot = 0.15 * layout.position_map.line_height;
 
         cx.scene.push_layer(Some(bounds));
 
@@ -714,12 +722,29 @@ impl EditorElement {
             },
         });
 
+        for (range, _) in layout.click_ranges.iter() {
+            for bound in range_to_bounds(
+                range,
+                content_origin,
+                scroll_left,
+                scroll_top,
+                &layout.visible_display_row_range,
+                line_end_overshoot,
+                &layout.position_map,
+            ) {
+                cx.scene.push_cursor_region(CursorRegion {
+                    bounds: bound,
+                    style: CursorStyle::PointingHand,
+                });
+            }
+        }
+
         for (range, color) in &layout.highlighted_ranges {
             self.paint_highlighted_range(
                 range.clone(),
                 *color,
                 0.,
-                0.15 * layout.position_map.line_height,
+                line_end_overshoot,
                 layout,
                 content_origin,
                 scroll_top,
@@ -1650,6 +1675,7 @@ impl Element for EditorElement {
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
         let mut highlighted_ranges = Vec::new();
+        let mut click_ranges = Vec::new();
         let mut show_scrollbars = false;
         let mut include_root = false;
         let mut is_singleton = false;
@@ -1662,6 +1688,7 @@ impl Element for EditorElement {
             let theme = cx.global::<Settings>().theme.as_ref();
             highlighted_ranges =
                 view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
+            click_ranges = view.click_ranges_in_range(start_anchor..end_anchor, &display_map);
 
             let mut remote_selections = HashMap::default();
             for (replica_id, line_mode, cursor_shape, selection) in display_map
@@ -1917,6 +1944,7 @@ impl Element for EditorElement {
                 active_rows,
                 highlighted_rows,
                 highlighted_ranges,
+                click_ranges: Arc::new(click_ranges),
                 line_number_layouts,
                 display_hunks,
                 blocks,
@@ -1948,6 +1976,7 @@ impl Element for EditorElement {
         Self::attach_mouse_handlers(
             &self.view,
             &layout.position_map,
+            layout.click_ranges.clone(), // No need to clone the vec
             layout.hover_popovers.is_some(),
             visible_bounds,
             text_bounds,
@@ -2045,6 +2074,7 @@ pub struct LayoutState {
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
+    click_ranges: Arc<Vec<(Range<DisplayPoint>, TextClickedCallback)>>,
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
@@ -2351,6 +2381,98 @@ impl HighlightedRange {
     }
 }
 
+pub trait ClickRange: 'static {
+    fn click_handler(
+        click: &MouseClick,
+        range: &Range<DisplayPoint>,
+        snapshot: &EditorSnapshot,
+        cx: &mut EventContext,
+    );
+}
+
+pub enum FoldMarker {}
+impl ClickRange for FoldMarker {
+    fn click_handler(
+        _click: &MouseClick,
+        range: &Range<DisplayPoint>,
+        _snapshot: &EditorSnapshot,
+        cx: &mut EventContext,
+    ) {
+        cx.dispatch_action(UnfoldAt {
+            display_row: DisplayRow(range.start.row()),
+        })
+    }
+}
+
+pub fn position_to_display_point(
+    position: Vector2F,
+    text_bounds: RectF,
+    position_map: &PositionMap,
+) -> Option<DisplayPoint> {
+    if text_bounds.contains_point(position) {
+        let (point, target_point) = position_map.point_for_position(text_bounds, position);
+        if point == target_point {
+            Some(point)
+        } else {
+            None
+        }
+    } else {
+        None
+    }
+}
+
+pub fn range_to_bounds(
+    range: &Range<DisplayPoint>,
+    content_origin: Vector2F,
+    scroll_left: f32,
+    scroll_top: f32,
+    visible_row_range: &Range<u32>,
+    line_end_overshoot: f32,
+    position_map: &PositionMap,
+) -> impl Iterator<Item = RectF> {
+    let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
+
+    if range.start == range.end {
+        return bounds.into_iter();
+    }
+
+    let start_row = visible_row_range.start;
+    let end_row = visible_row_range.end;
+
+    let row_range = if range.end.column() == 0 {
+        cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+    } else {
+        cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+    };
+
+    let first_y =
+        content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
+
+    for (idx, row) in row_range.enumerate() {
+        let line_layout = &position_map.line_layouts[(row - start_row) as usize];
+
+        let start_x = if row == range.start.row() {
+            content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
+                - scroll_left
+        } else {
+            content_origin.x() - scroll_left
+        };
+
+        let end_x = if row == range.end.row() {
+            content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
+        } else {
+            content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
+        };
+
+        bounds.push(RectF::from_points(
+            vec2f(start_x, first_y + position_map.line_height * idx as f32),
+            vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
+        ))
+    }
+
+    bounds.into_iter()
+}
+
 pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
     delta.powf(1.5) / 100.0
 }