update line layout to include multiple segments

Thomas Heartman created

Change summary

crates/editor/src/element.rs | 197 +++++++++++++++++++++----------------
1 file changed, 109 insertions(+), 88 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -763,8 +763,14 @@ impl EditorElement {
                 .row;
             if line_numbers
                 .get(&MultiBufferRow(multi_buffer_row))
-                .and_then(|line_number| line_number.hitbox.as_ref())
-                .is_some_and(|hitbox| hitbox.contains(&event.position))
+                .is_some_and(|line_layout| {
+                    line_layout.segments.iter().any(|segment| {
+                        segment
+                            .hitbox
+                            .as_ref()
+                            .is_some_and(|hitbox| hitbox.contains(&event.position))
+                    })
+                })
             {
                 let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
 
@@ -3249,70 +3255,79 @@ impl EditorElement {
         let relative_rows =
             self.calculate_relative_line_numbers(snapshot, &rows, relative_to, true);
         let mut line_number = String::new();
-        let line_numbers = buffer_rows
-            .iter()
-            .enumerate()
-            .flat_map(|(ix, row_info)| {
-                let display_row = DisplayRow(rows.start.0 + ix as u32);
-                line_number.clear();
-                let number = if let Some(relative_number) = relative_rows.get(&display_row) {
-                    *relative_number
-                } else {
-                    row_info.buffer_row? + 1
-                };
-
-                write!(&mut line_number, "{number}").unwrap();
-                if row_info
-                    .diff_status
-                    .is_some_and(|status| status.is_deleted())
-                {
-                    return None;
-                }
+        let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| {
+            let display_row = DisplayRow(rows.start.0 + ix as u32);
+            line_number.clear();
+            let number = if let Some(relative_number) = relative_rows.get(&display_row) {
+                *relative_number
+            } else {
+                row_info.buffer_row? + 1
+            };
 
-                let color = active_rows
-                    .get(&display_row)
-                    .map(|spec| {
-                        if spec.breakpoint {
-                            cx.theme().colors().debugger_accent
-                        } else {
-                            cx.theme().colors().editor_active_line_number
-                        }
-                    })
-                    .unwrap_or_else(|| cx.theme().colors().editor_line_number);
-                let shaped_line =
-                    self.shape_line_number(SharedString::from(&line_number), color, window);
-                let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
-                let line_origin = gutter_hitbox.map(|hitbox| {
-                    hitbox.origin
-                        + point(
-                            hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
-                            ix as f32 * line_height
-                                - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
-                        )
-                });
+            write!(&mut line_number, "{number}").unwrap();
+            if row_info
+                .diff_status
+                .is_some_and(|status| status.is_deleted())
+            {
+                return None;
+            }
 
-                #[cfg(not(test))]
-                let hitbox = line_origin.map(|line_origin| {
-                    window.insert_hitbox(
-                        Bounds::new(line_origin, size(shaped_line.width, line_height)),
-                        HitboxBehavior::Normal,
+            let color = active_rows
+                .get(&display_row)
+                .map(|spec| {
+                    if spec.breakpoint {
+                        cx.theme().colors().debugger_accent
+                    } else {
+                        cx.theme().colors().editor_active_line_number
+                    }
+                })
+                .unwrap_or_else(|| cx.theme().colors().editor_line_number);
+            let shaped_line =
+                self.shape_line_number(SharedString::from(&line_number), color, window);
+            let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
+            let line_origin = gutter_hitbox.map(|hitbox| {
+                hitbox.origin
+                    + point(
+                        hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
+                        ix as f32 * line_height
+                            - Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
                     )
-                });
-                #[cfg(test)]
-                let hitbox = {
-                    let _ = line_origin;
-                    None
-                };
+            });
 
-                let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
-                let multi_buffer_row = MultiBufferRow(multi_buffer_row);
-                let line_number = LineNumberLayout {
-                    shaped_line,
-                    hitbox,
-                };
-                Some((multi_buffer_row, line_number))
-            })
-            .collect();
+            #[cfg(not(test))]
+            let hitbox = line_origin.map(|line_origin| {
+                window.insert_hitbox(
+                    Bounds::new(line_origin, size(shaped_line.width, line_height)),
+                    HitboxBehavior::Normal,
+                )
+            });
+            #[cfg(test)]
+            let hitbox = {
+                let _ = line_origin;
+                None
+            };
+
+            let segment = LineNumberSegment {
+                shaped_line,
+                hitbox,
+            };
+
+            let buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
+            let multi_buffer_row = MultiBufferRow(buffer_row);
+
+            Some((multi_buffer_row, segment))
+        });
+
+        let mut line_numbers: HashMap<MultiBufferRow, LineNumberLayout> = HashMap::default();
+        for (buffer_row, segment) in segments {
+            line_numbers
+                .entry(buffer_row)
+                .or_insert_with(|| LineNumberLayout {
+                    segments: Vec::new(),
+                })
+                .segments
+                .push(segment);
+        }
         Arc::new(line_numbers)
     }
 
@@ -5861,34 +5876,36 @@ impl EditorElement {
         let line_height = layout.position_map.line_height;
         window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
 
-        for LineNumberLayout {
-            shaped_line,
-            hitbox,
-        } in layout.line_numbers.values()
-        {
-            let Some(hitbox) = hitbox else {
-                continue;
-            };
+        for line_layout in layout.line_numbers.values() {
+            for LineNumberSegment {
+                shaped_line,
+                hitbox,
+            } in &line_layout.segments
+            {
+                let Some(hitbox) = hitbox else {
+                    continue;
+                };
 
-            let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
-                let color = cx.theme().colors().editor_hover_line_number;
+                let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
+                    let color = cx.theme().colors().editor_hover_line_number;
 
-                let line = self.shape_line_number(shaped_line.text.clone(), color, window);
-                line.paint(hitbox.origin, line_height, window, cx).log_err()
-            } else {
-                shaped_line
-                    .paint(hitbox.origin, line_height, window, cx)
-                    .log_err()
-            }) else {
-                continue;
-            };
+                    let line = self.shape_line_number(shaped_line.text.clone(), color, window);
+                    line.paint(hitbox.origin, line_height, window, cx).log_err()
+                } else {
+                    shaped_line
+                        .paint(hitbox.origin, line_height, window, cx)
+                        .log_err()
+                }) else {
+                    continue;
+                };
 
-            // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
-            // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
-            if is_singleton {
-                window.set_cursor_style(CursorStyle::IBeam, hitbox);
-            } else {
-                window.set_cursor_style(CursorStyle::PointingHand, hitbox);
+                // In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
+                // In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
+                if is_singleton {
+                    window.set_cursor_style(CursorStyle::IBeam, hitbox);
+                } else {
+                    window.set_cursor_style(CursorStyle::PointingHand, hitbox);
+                }
             }
         }
     }
@@ -9798,11 +9815,15 @@ impl EditorLayout {
     }
 }
 
-struct LineNumberLayout {
+struct LineNumberSegment {
     shaped_line: ShapedLine,
     hitbox: Option<Hitbox>,
 }
 
+struct LineNumberLayout {
+    segments: Vec<LineNumberSegment>,
+}
+
 struct ColoredRange<T> {
     start: T,
     end: T,