Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:36:07.471Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

mouse.rs

1289 lines · 50.4 KB · rust
1use std::ops::Range;
2use std::time::{Duration, Instant};
3
4use collections::HashMap;
5use feature_flags::{DiffReviewFeatureFlag, FeatureFlagAppExt as _};
6use gpui::{
7    AnyElement, App, AvailableSpace, ClickEvent, Context, DefiniteLength, DispatchPhase, Element,
8    MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
9    ParentElement, Pixels, PressureStage, ScrollDelta, ScrollWheelEvent, TextStyleRefinement,
10    Window, anchored, deferred, point, px,
11};
12use multi_buffer::MultiBufferRow;
13use project::DisableAiSettings;
14use settings::Settings;
15use sum_tree::Bias;
16use text::SelectionGoal;
17use theme_settings::BufferLineHeight;
18use util::{RangeExt, debug_panic, post_inc};
19
20use super::{EditorElement, EditorLayout, LineNumberLayout, PositionMap, SplitSide};
21use crate::{
22    CURSORS_VISIBLE_FOR, ColumnarMode, DisplayDiffHunk, DisplayPoint, DisplayRow, Editor,
23    EditorSettings, EditorSnapshot, GutterHoverButton, HoveredCursor, JumpData,
24    PhantomDiffReviewIndicator, SelectPhase, Selection, SelectionDragState,
25    display_map::ToDisplayPoint, editor_settings::DoubleClickInMultibuffer,
26    hover_popover::hover_at, mouse_context_menu, scroll::ScrollPixelOffset,
27};
28
29impl EditorElement {
30    pub(crate) fn mouse_moved(
31        editor: &mut Editor,
32        event: &MouseMoveEvent,
33        position_map: &PositionMap,
34        split_side: Option<SplitSide>,
35        window: &mut Window,
36        cx: &mut Context<Editor>,
37    ) {
38        let text_hitbox = &position_map.text_hitbox;
39        let gutter_hitbox = &position_map.gutter_hitbox;
40        let modifiers = event.modifiers;
41        let text_hovered = text_hitbox.is_hovered(window);
42        let gutter_hovered = gutter_hitbox.is_hovered(window);
43        editor.set_gutter_hovered(gutter_hovered, cx);
44
45        let point_for_position = position_map.point_for_position(event.position);
46        let valid_point = point_for_position.nearest_valid;
47
48        // Update diff review drag state if we're dragging
49        if editor.diff_review_drag_state.is_some() {
50            editor.update_diff_review_drag(valid_point.row(), window, cx);
51        }
52
53        let hovered_diff_control = position_map
54            .diff_hunk_control_bounds
55            .iter()
56            .find(|(_, bounds)| bounds.contains(&event.position))
57            .map(|(row, _)| *row);
58
59        let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control {
60            Some(control_row)
61        } else if text_hovered {
62            let current_row = valid_point.row();
63            position_map.display_hunks.iter().find_map(|(hunk, _)| {
64                if let DisplayDiffHunk::Unfolded {
65                    display_row_range, ..
66                } = hunk
67                {
68                    if display_row_range.contains(&current_row) {
69                        Some(display_row_range.start)
70                    } else {
71                        None
72                    }
73                } else {
74                    None
75                }
76            })
77        } else {
78            None
79        };
80
81        if hovered_diff_hunk_row != editor.hovered_diff_hunk_row {
82            editor.hovered_diff_hunk_row = hovered_diff_hunk_row;
83            cx.notify();
84        }
85
86        if text_hovered
87            && let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds
88        {
89            let mouse_over_inline_blame = bounds.contains(&event.position);
90            let mouse_over_popover = editor
91                .inline_blame_popover
92                .as_ref()
93                .and_then(|state| state.popover_bounds)
94                .is_some_and(|bounds| bounds.contains(&event.position));
95            let keyboard_grace = editor
96                .inline_blame_popover
97                .as_ref()
98                .is_some_and(|state| state.keyboard_grace);
99
100            if mouse_over_inline_blame || mouse_over_popover {
101                editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx);
102            } else if !keyboard_grace {
103                editor.hide_blame_popover(false, cx);
104            }
105        } else {
106            let keyboard_grace = editor
107                .inline_blame_popover
108                .as_ref()
109                .is_some_and(|state| state.keyboard_grace);
110            if !keyboard_grace {
111                editor.hide_blame_popover(false, cx);
112            }
113        }
114
115        // Handle diff review indicator when gutter is hovered in diff mode with AI enabled
116        let show_diff_review = editor.show_diff_review_button()
117            && cx.has_flag::<DiffReviewFeatureFlag>()
118            && !DisableAiSettings::is_ai_disabled_for_buffer(
119                editor.buffer.read(cx).as_singleton().as_ref(),
120                cx,
121            );
122
123        let diff_review_indicator = if gutter_hovered && show_diff_review {
124            let is_visible = editor
125                .gutter_diff_review_indicator
126                .0
127                .is_some_and(|indicator| indicator.is_active);
128
129            if !is_visible {
130                editor
131                    .gutter_diff_review_indicator
132                    .1
133                    .get_or_insert_with(|| {
134                        cx.spawn(async move |this, cx| {
135                            cx.background_executor()
136                                .timer(Duration::from_millis(200))
137                                .await;
138
139                            this.update(cx, |this, cx| {
140                                if let Some(indicator) =
141                                    this.gutter_diff_review_indicator.0.as_mut()
142                                {
143                                    indicator.is_active = true;
144                                    cx.notify();
145                                }
146                            })
147                            .ok();
148                        })
149                    });
150            }
151
152            let anchor = position_map
153                .snapshot
154                .display_point_to_anchor(valid_point, Bias::Left);
155            Some(PhantomDiffReviewIndicator {
156                start: anchor,
157                end: anchor,
158                is_active: is_visible,
159            })
160        } else {
161            editor.gutter_diff_review_indicator.1 = None;
162            None
163        };
164
165        if diff_review_indicator != editor.gutter_diff_review_indicator.0 {
166            editor.gutter_diff_review_indicator.0 = diff_review_indicator;
167            cx.notify();
168        }
169
170        // Don't show breakpoint indicator when diff review indicator is active on this row
171        let is_on_diff_review_button_row = diff_review_indicator.is_some_and(|indicator| {
172            let start_row = indicator
173                .start
174                .to_display_point(&position_map.snapshot.display_snapshot)
175                .row();
176            indicator.is_active && start_row == valid_point.row()
177        });
178
179        let gutter_hover_button = if gutter_hovered
180            && !is_on_diff_review_button_row
181            && split_side != Some(SplitSide::Left)
182        {
183            let buffer_anchor = position_map
184                .snapshot
185                .display_point_to_anchor(valid_point, Bias::Left);
186
187            // Breakpoints and bookmarks are keyed by absolute file path, so the
188            // gutter button would be a no-op for buffers without a worktree file
189            // (e.g. untitled buffers). Hide it there.
190            if position_map
191                .snapshot
192                .buffer_snapshot()
193                .anchor_to_buffer_anchor(buffer_anchor)
194                .is_some_and(|(_, buffer_snapshot)| {
195                    project::File::from_dyn(buffer_snapshot.file()).is_some()
196                })
197            {
198                let is_visible = editor
199                    .gutter_hover_button
200                    .0
201                    .is_some_and(|indicator| indicator.is_active);
202
203                if !is_visible {
204                    editor.gutter_hover_button.1.get_or_insert_with(|| {
205                        cx.spawn(async move |this, cx| {
206                            cx.background_executor()
207                                .timer(Duration::from_millis(200))
208                                .await;
209
210                            this.update(cx, |this, cx| {
211                                if let Some(indicator) = this.gutter_hover_button.0.as_mut() {
212                                    indicator.is_active = true;
213                                    cx.notify();
214                                }
215                            })
216                            .ok();
217                        })
218                    });
219                }
220
221                Some(GutterHoverButton {
222                    display_row: valid_point.row(),
223                    is_active: is_visible,
224                })
225            } else {
226                editor.gutter_hover_button.1 = None;
227                None
228            }
229        } else if editor.has_mouse_context_menu() {
230            editor.gutter_hover_button.1 = None;
231            editor.gutter_hover_button.0
232        } else {
233            editor.gutter_hover_button.1 = None;
234            None
235        };
236
237        if &gutter_hover_button != &editor.gutter_hover_button.0 {
238            editor.gutter_hover_button.0 = gutter_hover_button;
239            cx.notify();
240        }
241
242        // Don't trigger hover popover if mouse is hovering over context menu
243        if text_hovered {
244            editor.update_hovered_link(
245                point_for_position,
246                Some(event.position),
247                &position_map.snapshot,
248                modifiers,
249                window,
250                cx,
251            );
252
253            if let Some(point) = point_for_position.as_valid() {
254                let anchor = position_map
255                    .snapshot
256                    .buffer_snapshot()
257                    .anchor_before(point.to_offset(&position_map.snapshot, Bias::Left));
258                hover_at(editor, Some(anchor), Some(event.position), window, cx);
259                Self::update_visible_cursor(editor, point, position_map, window, cx);
260            } else {
261                editor.update_inlay_link_and_hover_points(
262                    &position_map.snapshot,
263                    point_for_position,
264                    Some(event.position),
265                    modifiers.secondary(),
266                    modifiers.shift,
267                    window,
268                    cx,
269                );
270            }
271        } else {
272            editor.hide_hovered_link(cx);
273            hover_at(editor, None, Some(event.position), window, cx);
274        }
275    }
276
277    pub(super) fn layout_mouse_context_menu(
278        &self,
279        editor_snapshot: &EditorSnapshot,
280        visible_range: Range<DisplayRow>,
281        content_origin: gpui::Point<Pixels>,
282        window: &mut Window,
283        cx: &mut App,
284    ) -> Option<AnyElement> {
285        let position = self.editor.update(cx, |editor, cx| {
286            let visible_start_point = editor.display_to_pixel_point(
287                DisplayPoint::new(visible_range.start, 0),
288                editor_snapshot,
289                window,
290                cx,
291            )?;
292            let visible_end_point = editor.display_to_pixel_point(
293                DisplayPoint::new(visible_range.end, 0),
294                editor_snapshot,
295                window,
296                cx,
297            )?;
298
299            let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
300            let (source_display_point, position) = match mouse_context_menu.position {
301                mouse_context_menu::MenuPosition::PinnedToScreen(point) => (None, point),
302                mouse_context_menu::MenuPosition::PinnedToEditor { source, offset } => {
303                    let source_display_point = source.to_display_point(editor_snapshot);
304                    let source_point =
305                        editor.to_pixel_point(source, editor_snapshot, window, cx)?;
306                    let position = content_origin + source_point + offset;
307                    (Some(source_display_point), position)
308                }
309            };
310
311            let source_included = source_display_point.is_none_or(|source_display_point| {
312                visible_range
313                    .to_inclusive()
314                    .contains(&source_display_point.row())
315            });
316            let position_included =
317                visible_start_point.y <= position.y && position.y <= visible_end_point.y;
318            if !source_included && !position_included {
319                None
320            } else {
321                Some(position)
322            }
323        })?;
324
325        let text_style = TextStyleRefinement {
326            line_height: Some(DefiniteLength::Fraction(
327                BufferLineHeight::Comfortable.value(),
328            )),
329            ..Default::default()
330        };
331        window.with_text_style(Some(text_style), |window| {
332            let mut element = self.editor.read_with(cx, |editor, _| {
333                let mouse_context_menu = editor.mouse_context_menu.as_ref()?;
334                let context_menu = mouse_context_menu.context_menu.clone();
335
336                Some(
337                    deferred(
338                        anchored()
339                            .position(position)
340                            .child(context_menu)
341                            .anchor(gpui::Anchor::TopLeft)
342                            .snap_to_window_with_margin(px(8.)),
343                    )
344                    .with_priority(1)
345                    .into_any(),
346                )
347            })?;
348
349            element.prepaint_as_root(position, AvailableSpace::min_size(), window, cx);
350            Some(element)
351        })
352    }
353
354    pub(super) fn paint_mouse_listeners(
355        &mut self,
356        layout: &EditorLayout,
357        window: &mut Window,
358        cx: &mut App,
359    ) {
360        if layout.mode.is_minimap() {
361            return;
362        }
363
364        self.paint_scroll_wheel_listener(layout, window, cx);
365
366        window.on_mouse_event({
367            let position_map = layout.position_map.clone();
368            let editor = self.editor.clone();
369            let line_numbers = layout.line_numbers.clone();
370
371            move |event: &MouseDownEvent, phase, window, cx| {
372                if phase == DispatchPhase::Bubble {
373                    match event.button {
374                        MouseButton::Left => editor.update(cx, |editor, cx| {
375                            let pending_mouse_down = editor
376                                .pending_mouse_down
377                                .get_or_insert_with(Default::default)
378                                .clone();
379
380                            *pending_mouse_down.borrow_mut() = Some(event.clone());
381
382                            Self::mouse_left_down(
383                                editor,
384                                event,
385                                &position_map,
386                                line_numbers.as_ref(),
387                                window,
388                                cx,
389                            );
390                        }),
391                        MouseButton::Right => editor.update(cx, |editor, cx| {
392                            Self::mouse_right_down(editor, event, &position_map, window, cx);
393                        }),
394                        MouseButton::Middle => editor.update(cx, |editor, cx| {
395                            Self::mouse_middle_down(editor, event, &position_map, window, cx);
396                        }),
397                        _ => {}
398                    };
399                }
400            }
401        });
402
403        window.on_mouse_event({
404            let editor = self.editor.clone();
405            let position_map = layout.position_map.clone();
406
407            move |event: &MouseUpEvent, phase, window, cx| {
408                if phase == DispatchPhase::Bubble {
409                    editor.update(cx, |editor, cx| {
410                        Self::mouse_up(editor, event, &position_map, window, cx)
411                    });
412                }
413            }
414        });
415
416        window.on_mouse_event({
417            let editor = self.editor.clone();
418            let position_map = layout.position_map.clone();
419            let mut captured_mouse_down = None;
420
421            move |event: &MouseUpEvent, phase, window, cx| match phase {
422                // Clear the pending mouse down during the capture phase,
423                // so that it happens even if another event handler stops
424                // propagation.
425                DispatchPhase::Capture => editor.update(cx, |editor, _cx| {
426                    let pending_mouse_down = editor
427                        .pending_mouse_down
428                        .get_or_insert_with(Default::default)
429                        .clone();
430
431                    let mut pending_mouse_down = pending_mouse_down.borrow_mut();
432                    if pending_mouse_down.is_some() && position_map.text_hitbox.is_hovered(window) {
433                        captured_mouse_down = pending_mouse_down.take();
434                        window.refresh();
435                    }
436                }),
437                // Fire click handlers during the bubble phase.
438                DispatchPhase::Bubble => editor.update(cx, |editor, cx| {
439                    if let Some(mouse_down) = captured_mouse_down.take() {
440                        let event = ClickEvent::Mouse(MouseClickEvent {
441                            down: mouse_down,
442                            up: event.clone(),
443                        });
444                        Self::click(editor, &event, &position_map, window, cx);
445                    }
446                }),
447            }
448        });
449
450        window.on_mouse_event({
451            let position_map = layout.position_map.clone();
452            let editor = self.editor.clone();
453
454            move |event: &MousePressureEvent, phase, window, cx| {
455                if phase == DispatchPhase::Bubble {
456                    editor.update(cx, |editor, cx| {
457                        Self::pressure_click(editor, &event, &position_map, window, cx);
458                    })
459                }
460            }
461        });
462
463        window.on_mouse_event({
464            let position_map = layout.position_map.clone();
465            let editor = self.editor.clone();
466            let split_side = self.split_side;
467
468            move |event: &MouseMoveEvent, phase, window, cx| {
469                if phase == DispatchPhase::Bubble {
470                    editor.update(cx, |editor, cx| {
471                        if editor.hover_state.focused(window, cx) {
472                            return;
473                        }
474                        if event.pressed_button == Some(MouseButton::Left)
475                            || event.pressed_button == Some(MouseButton::Middle)
476                        {
477                            Self::mouse_dragged(editor, event, &position_map, window, cx)
478                        }
479
480                        Self::mouse_moved(editor, event, &position_map, split_side, window, cx)
481                    });
482                }
483            }
484        });
485    }
486
487    fn paint_scroll_wheel_listener(
488        &mut self,
489        layout: &EditorLayout,
490        window: &mut Window,
491        cx: &mut App,
492    ) {
493        window.on_mouse_event({
494            let position_map = layout.position_map.clone();
495            let editor = self.editor.clone();
496            let hitbox = layout.hitbox.clone();
497            let mut delta = ScrollDelta::default();
498
499            // Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
500            // accidentally turn off their scrolling.
501            let base_scroll_sensitivity =
502                EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
503
504            // Use a minimum fast_scroll_sensitivity for same reason above
505            let fast_scroll_sensitivity = EditorSettings::get_global(cx)
506                .fast_scroll_sensitivity
507                .max(0.01);
508
509            move |event: &ScrollWheelEvent, phase, window, cx| {
510                if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) {
511                    delta = delta.coalesce(event.delta);
512
513                    if event.modifiers.secondary()
514                        && editor.read(cx).enable_mouse_wheel_zoom
515                        && EditorSettings::get_global(cx).mouse_wheel_zoom
516                    {
517                        let delta_y = match event.delta {
518                            ScrollDelta::Pixels(pixels) => pixels.y.into(),
519                            ScrollDelta::Lines(lines) => lines.y,
520                        };
521
522                        if delta_y > 0.0 {
523                            theme_settings::increase_buffer_font_size(cx);
524                        } else if delta_y < 0.0 {
525                            theme_settings::decrease_buffer_font_size(cx);
526                        }
527
528                        cx.stop_propagation();
529                    } else {
530                        let scroll_sensitivity = {
531                            if event.modifiers.alt {
532                                fast_scroll_sensitivity
533                            } else {
534                                base_scroll_sensitivity
535                            }
536                        };
537
538                        editor.update(cx, |editor, cx| {
539                            let line_height = position_map.line_height;
540                            let glyph_width = position_map.em_layout_width;
541                            let (delta, axis) = match delta {
542                                gpui::ScrollDelta::Pixels(mut pixels) => {
543                                    //Trackpad
544                                    let axis =
545                                        position_map.snapshot.ongoing_scroll.filter(&mut pixels);
546                                    (pixels, axis)
547                                }
548
549                                gpui::ScrollDelta::Lines(lines) => {
550                                    //Not trackpad
551                                    let pixels =
552                                        point(lines.x * glyph_width, lines.y * line_height);
553                                    (pixels, None)
554                                }
555                            };
556
557                            let current_scroll_position = position_map.snapshot.scroll_position();
558                            let x = (current_scroll_position.x
559                                * ScrollPixelOffset::from(glyph_width)
560                                - ScrollPixelOffset::from(delta.x * scroll_sensitivity))
561                                / ScrollPixelOffset::from(glyph_width);
562                            let y = (current_scroll_position.y
563                                * ScrollPixelOffset::from(line_height)
564                                - ScrollPixelOffset::from(delta.y * scroll_sensitivity))
565                                / ScrollPixelOffset::from(line_height);
566                            let mut scroll_position =
567                                point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
568                            let forbid_vertical_scroll =
569                                editor.scroll_manager.forbid_vertical_scroll();
570                            if forbid_vertical_scroll {
571                                scroll_position.y = current_scroll_position.y;
572                            }
573
574                            if scroll_position != current_scroll_position {
575                                editor.scroll(scroll_position, axis, window, cx);
576                                cx.stop_propagation();
577                            } else if y < 0. && !forbid_vertical_scroll {
578                                // Due to clamping, we may fail to detect cases of overscroll to the top;
579                                // We want the scroll manager to get an update in such cases and detect the change of direction
580                                // on the next frame.
581                                if editor.scroll_manager.should_notify_top_overscroll(axis) {
582                                    cx.notify();
583                                }
584                            } else {
585                                editor.scroll_manager.reset_top_overscroll_notification();
586                            }
587                        });
588                    }
589                }
590            }
591        });
592    }
593
594    fn mouse_left_down(
595        editor: &mut Editor,
596        event: &MouseDownEvent,
597        position_map: &PositionMap,
598        line_numbers: &HashMap<MultiBufferRow, LineNumberLayout>,
599        window: &mut Window,
600        cx: &mut Context<Editor>,
601    ) {
602        if window.default_prevented() {
603            return;
604        }
605
606        let text_hitbox = &position_map.text_hitbox;
607        let gutter_hitbox = &position_map.gutter_hitbox;
608        let point_for_position = position_map.point_for_position(event.position);
609        let mut click_count = event.click_count;
610        let mut modifiers = event.modifiers;
611
612        if let Some(hovered_hunk) =
613            position_map
614                .display_hunks
615                .iter()
616                .find_map(|(hunk, hunk_hitbox)| match hunk {
617                    DisplayDiffHunk::Folded { .. } => None,
618                    DisplayDiffHunk::Unfolded {
619                        multi_buffer_range, ..
620                    } => hunk_hitbox
621                        .as_ref()
622                        .is_some_and(|hitbox| hitbox.is_hovered(window))
623                        .then(|| multi_buffer_range.clone()),
624                })
625        {
626            editor.toggle_single_diff_hunk(hovered_hunk, cx);
627            cx.notify();
628            return;
629        } else if gutter_hitbox.is_hovered(window) {
630            click_count = 3; // Simulate triple-click when clicking the gutter to select lines
631        } else if !text_hitbox.is_hovered(window) {
632            return;
633        }
634
635        if EditorSettings::get_global(cx)
636            .drag_and_drop_selection
637            .enabled
638            && click_count == 1
639            && !modifiers.shift
640        {
641            let newest_anchor = editor.selections.newest_anchor();
642            let snapshot = editor.snapshot(window, cx);
643            let selection = newest_anchor.map(|anchor| anchor.to_display_point(&snapshot));
644            if point_for_position.intersects_selection(&selection) {
645                editor.selection_drag_state = SelectionDragState::ReadyToDrag {
646                    selection: newest_anchor.clone(),
647                    click_position: event.position,
648                    mouse_down_time: Instant::now(),
649                };
650                cx.stop_propagation();
651                return;
652            }
653        }
654
655        let is_singleton = editor.buffer().read(cx).is_singleton();
656
657        if click_count == 2 && !is_singleton {
658            match EditorSettings::get_global(cx).double_click_in_multibuffer {
659                DoubleClickInMultibuffer::Select => {
660                    // do nothing special on double click, all selection logic is below
661                }
662                DoubleClickInMultibuffer::Open => {
663                    if modifiers.alt {
664                        // if double click is made with alt, pretend it's a regular double click without opening and alt,
665                        // and run the selection logic.
666                        modifiers.alt = false;
667                    } else {
668                        let scroll_position_row = position_map.scroll_position.y;
669                        let display_row = (((event.position - gutter_hitbox.bounds.origin).y
670                            / position_map.line_height)
671                            as f64
672                            + position_map.scroll_position.y)
673                            as u32;
674                        let multi_buffer_row = position_map
675                            .snapshot
676                            .display_point_to_point(
677                                DisplayPoint::new(DisplayRow(display_row), 0),
678                                Bias::Right,
679                            )
680                            .row;
681                        let line_offset_from_top = display_row - scroll_position_row as u32;
682                        // if double click is made without alt, open the corresponding excerp
683                        editor.open_excerpts_common(
684                            Some(JumpData::MultiBufferRow {
685                                row: MultiBufferRow(multi_buffer_row),
686                                line_offset_from_top,
687                            }),
688                            false,
689                            window,
690                            cx,
691                        );
692                        return;
693                    }
694                }
695            }
696        }
697
698        if !is_singleton {
699            let display_row = (ScrollPixelOffset::from(
700                (event.position - gutter_hitbox.bounds.origin).y / position_map.line_height,
701            ) + position_map.scroll_position.y) as u32;
702            let multi_buffer_row = position_map
703                .snapshot
704                .display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right)
705                .row;
706            if line_numbers
707                .get(&MultiBufferRow(multi_buffer_row))
708                .is_some_and(|line_layout| {
709                    line_layout.segments.iter().any(|segment| {
710                        segment
711                            .hitbox
712                            .as_ref()
713                            .is_some_and(|hitbox| hitbox.contains(&event.position))
714                    })
715                })
716            {
717                let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
718
719                editor.open_excerpts_common(
720                    Some(JumpData::MultiBufferRow {
721                        row: MultiBufferRow(multi_buffer_row),
722                        line_offset_from_top,
723                    }),
724                    modifiers.alt,
725                    window,
726                    cx,
727                );
728                cx.stop_propagation();
729                return;
730            }
731        }
732
733        let position = point_for_position.nearest_valid;
734        if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) {
735            editor.select(
736                SelectPhase::BeginColumnar {
737                    position,
738                    reset: match mode {
739                        ColumnarMode::FromMouse => true,
740                        ColumnarMode::FromSelection => false,
741                    },
742                    mode,
743                    goal_column: point_for_position.exact_unclipped.column(),
744                },
745                window,
746                cx,
747            );
748        } else if modifiers.shift && !modifiers.control && !modifiers.alt && !modifiers.secondary()
749        {
750            editor.select(
751                SelectPhase::Extend {
752                    position,
753                    click_count,
754                },
755                window,
756                cx,
757            );
758        } else {
759            editor.select(
760                SelectPhase::Begin {
761                    position,
762                    add: Editor::is_alt_pressed(&modifiers, cx),
763                    click_count,
764                },
765                window,
766                cx,
767            );
768        }
769        cx.stop_propagation();
770    }
771
772    fn mouse_right_down(
773        editor: &mut Editor,
774        event: &MouseDownEvent,
775        position_map: &PositionMap,
776        window: &mut Window,
777        cx: &mut Context<Editor>,
778    ) {
779        if position_map.gutter_hitbox.is_hovered(window) {
780            let gutter_right_padding = editor.gutter_dimensions.right_padding;
781            let hitbox = &position_map.gutter_hitbox;
782
783            if event.position.x <= hitbox.bounds.right() - gutter_right_padding
784                // Don't show the gutter_context_menu in collab notes
785                && editor.project.is_some()
786            {
787                let point_for_position = position_map.point_for_position(event.position);
788                editor.set_gutter_context_menu(
789                    point_for_position.nearest_valid.row(),
790                    None,
791                    event.position,
792                    window,
793                    cx,
794                );
795            }
796            return;
797        }
798
799        if !position_map.text_hitbox.is_hovered(window) {
800            return;
801        }
802
803        let point_for_position = position_map.point_for_position(event.position);
804        mouse_context_menu::deploy_context_menu(
805            editor,
806            Some(event.position),
807            point_for_position.nearest_valid,
808            window,
809            cx,
810        );
811        cx.stop_propagation();
812    }
813
814    fn mouse_middle_down(
815        editor: &mut Editor,
816        event: &MouseDownEvent,
817        position_map: &PositionMap,
818        window: &mut Window,
819        cx: &mut Context<Editor>,
820    ) {
821        if !position_map.text_hitbox.is_hovered(window) || window.default_prevented() {
822            return;
823        }
824
825        let point_for_position = position_map.point_for_position(event.position);
826        let position = point_for_position.nearest_valid;
827
828        editor.select(
829            SelectPhase::BeginColumnar {
830                position,
831                reset: true,
832                mode: ColumnarMode::FromMouse,
833                goal_column: point_for_position.exact_unclipped.column(),
834            },
835            window,
836            cx,
837        );
838    }
839
840    fn mouse_up(
841        editor: &mut Editor,
842        event: &MouseUpEvent,
843        position_map: &PositionMap,
844        window: &mut Window,
845        cx: &mut Context<Editor>,
846    ) {
847        // Handle diff review drag completion
848        if editor.diff_review_drag_state.is_some() {
849            editor.end_diff_review_drag(window, cx);
850            cx.stop_propagation();
851            return;
852        }
853
854        let text_hitbox = &position_map.text_hitbox;
855        let end_selection = editor.has_pending_selection();
856        let pending_nonempty_selections = editor.has_pending_nonempty_selection();
857        let point_for_position = position_map.point_for_position(event.position);
858
859        match editor.selection_drag_state {
860            SelectionDragState::ReadyToDrag {
861                selection: _,
862                ref click_position,
863                mouse_down_time: _,
864            } => {
865                if event.position == *click_position {
866                    editor.select(
867                        SelectPhase::Begin {
868                            position: point_for_position.nearest_valid,
869                            add: false,
870                            click_count: 1, // ready to drag state only occurs on click count 1
871                        },
872                        window,
873                        cx,
874                    );
875                    editor.selection_drag_state = SelectionDragState::None;
876                    cx.stop_propagation();
877                    return;
878                } else {
879                    debug_panic!("drag state can never be in ready state after drag")
880                }
881            }
882            SelectionDragState::Dragging { ref selection, .. } => {
883                let snapshot = editor.snapshot(window, cx);
884                let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
885                if !point_for_position.intersects_selection(&selection_display)
886                    && text_hitbox.is_hovered(window)
887                {
888                    let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt
889                        || cfg!(not(target_os = "macos")) && event.modifiers.control);
890                    editor.move_selection_on_drop(
891                        &selection.clone(),
892                        point_for_position.nearest_valid,
893                        is_cut,
894                        window,
895                        cx,
896                    );
897                }
898                editor.selection_drag_state = SelectionDragState::None;
899                cx.stop_propagation();
900                cx.notify();
901                return;
902            }
903            _ => {}
904        }
905
906        if end_selection {
907            editor.select(SelectPhase::End, window, cx);
908        }
909
910        if end_selection && pending_nonempty_selections {
911            cx.stop_propagation();
912        } else if cfg!(any(target_os = "linux", target_os = "freebsd"))
913            && event.button == MouseButton::Middle
914        {
915            #[allow(
916                clippy::collapsible_if,
917                clippy::needless_return,
918                reason = "The cfg-block below makes this a false positive"
919            )]
920            if !text_hitbox.is_hovered(window) || editor.read_only(cx) {
921                return;
922            }
923
924            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
925            if EditorSettings::get_global(cx).middle_click_paste {
926                if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
927                    let point_for_position = position_map.point_for_position(event.position);
928                    let position = point_for_position.nearest_valid;
929
930                    editor.select(
931                        SelectPhase::Begin {
932                            position,
933                            add: false,
934                            click_count: 1,
935                        },
936                        window,
937                        cx,
938                    );
939                    editor.insert(&text, window, cx);
940                }
941                cx.stop_propagation()
942            }
943        }
944    }
945
946    fn click(
947        editor: &mut Editor,
948        event: &ClickEvent,
949        position_map: &PositionMap,
950        window: &mut Window,
951        cx: &mut Context<Editor>,
952    ) {
953        let text_hitbox = &position_map.text_hitbox;
954        let pending_nonempty_selections = editor.has_pending_nonempty_selection();
955
956        let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx);
957        let mouse_down_hovered_link_modifier = if let ClickEvent::Mouse(mouse_event) = event {
958            Editor::is_cmd_or_ctrl_pressed(&mouse_event.down.modifiers, cx)
959        } else {
960            true
961        };
962
963        if let Some(mouse_position) = event.mouse_position()
964            && !pending_nonempty_selections
965            && hovered_link_modifier
966            && mouse_down_hovered_link_modifier
967            && text_hitbox.is_hovered(window)
968            && !matches!(
969                editor.selection_drag_state,
970                SelectionDragState::Dragging { .. }
971            )
972        {
973            let point = position_map.point_for_position(mouse_position);
974            editor.handle_click_hovered_link(point, event.modifiers(), window, cx);
975            editor.selection_drag_state = SelectionDragState::None;
976
977            cx.stop_propagation();
978        }
979    }
980
981    fn pressure_click(
982        editor: &mut Editor,
983        event: &MousePressureEvent,
984        position_map: &PositionMap,
985        window: &mut Window,
986        cx: &mut Context<Editor>,
987    ) {
988        let text_hitbox = &position_map.text_hitbox;
989        let force_click_possible =
990            matches!(editor.prev_pressure_stage, Some(PressureStage::Normal))
991                && event.stage == PressureStage::Force;
992
993        editor.prev_pressure_stage = Some(event.stage);
994
995        if force_click_possible && text_hitbox.is_hovered(window) {
996            let point = position_map.point_for_position(event.position);
997            editor.handle_click_hovered_link(point, event.modifiers, window, cx);
998            editor.selection_drag_state = SelectionDragState::None;
999            cx.stop_propagation();
1000        }
1001    }
1002
1003    fn mouse_dragged(
1004        editor: &mut Editor,
1005        event: &MouseMoveEvent,
1006        position_map: &PositionMap,
1007        window: &mut Window,
1008        cx: &mut Context<Editor>,
1009    ) {
1010        if editor.has_autoscroll_request()
1011            || !editor.has_pending_selection()
1012                && matches!(editor.selection_drag_state, SelectionDragState::None)
1013        {
1014            return;
1015        }
1016
1017        let point_for_position = position_map.point_for_position(event.position);
1018        let text_hitbox = &position_map.text_hitbox;
1019
1020        let scroll_delta = {
1021            let text_bounds = text_hitbox.bounds;
1022            let mut scroll_delta = gpui::Point::<f32>::default();
1023            let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
1024            let top = text_bounds.origin.y + vertical_margin;
1025            let bottom = text_bounds.bottom_left().y - vertical_margin;
1026            if event.position.y < top {
1027                scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
1028            }
1029            if event.position.y > bottom {
1030                scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
1031            }
1032
1033            // We need horizontal width of text
1034            let style = editor.style.clone().unwrap_or_default();
1035            let font_id = window.text_system().resolve_font(&style.text.font());
1036            let font_size = style.text.font_size.to_pixels(window.rem_size());
1037            let em_width = window
1038                .text_system()
1039                .em_width(font_id, font_size)
1040                .unwrap_or(font_size);
1041
1042            let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
1043
1044            let scroll_space: Pixels = scroll_margin_x * em_width;
1045
1046            let left = text_bounds.origin.x + scroll_space;
1047            let right = text_bounds.top_right().x - scroll_space;
1048
1049            if event.position.x < left {
1050                scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
1051            }
1052            if event.position.x > right {
1053                scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
1054            }
1055            scroll_delta
1056        };
1057
1058        if !editor.has_pending_selection() {
1059            let drop_anchor = position_map
1060                .snapshot
1061                .display_point_to_anchor(point_for_position.nearest_valid, Bias::Left);
1062            match editor.selection_drag_state {
1063                SelectionDragState::Dragging {
1064                    ref mut drop_cursor,
1065                    ref mut hide_drop_cursor,
1066                    ..
1067                } => {
1068                    drop_cursor.start = drop_anchor;
1069                    drop_cursor.end = drop_anchor;
1070                    *hide_drop_cursor = !text_hitbox.is_hovered(window);
1071                    editor.apply_scroll_delta(scroll_delta, window, cx);
1072                    cx.notify();
1073                }
1074                SelectionDragState::ReadyToDrag {
1075                    ref selection,
1076                    ref click_position,
1077                    ref mouse_down_time,
1078                } => {
1079                    let drag_and_drop_delay = Duration::from_millis(
1080                        EditorSettings::get_global(cx)
1081                            .drag_and_drop_selection
1082                            .delay
1083                            .0,
1084                    );
1085                    if mouse_down_time.elapsed() >= drag_and_drop_delay {
1086                        let drop_cursor = Selection {
1087                            id: post_inc(&mut editor.selections.next_selection_id()),
1088                            start: drop_anchor,
1089                            end: drop_anchor,
1090                            reversed: false,
1091                            goal: SelectionGoal::None,
1092                        };
1093                        editor.selection_drag_state = SelectionDragState::Dragging {
1094                            selection: selection.clone(),
1095                            drop_cursor,
1096                            hide_drop_cursor: false,
1097                        };
1098                        editor.apply_scroll_delta(scroll_delta, window, cx);
1099                        cx.notify();
1100                    } else {
1101                        let click_point = position_map.point_for_position(*click_position);
1102                        editor.selection_drag_state = SelectionDragState::None;
1103                        editor.select(
1104                            SelectPhase::Begin {
1105                                position: click_point.nearest_valid,
1106                                add: false,
1107                                click_count: 1,
1108                            },
1109                            window,
1110                            cx,
1111                        );
1112                        editor.select(
1113                            SelectPhase::Update {
1114                                position: point_for_position.nearest_valid,
1115                                goal_column: point_for_position.exact_unclipped.column(),
1116                                scroll_delta,
1117                            },
1118                            window,
1119                            cx,
1120                        );
1121                    }
1122                }
1123                _ => {}
1124            }
1125        } else {
1126            editor.select(
1127                SelectPhase::Update {
1128                    position: point_for_position.nearest_valid,
1129                    goal_column: point_for_position.exact_unclipped.column(),
1130                    scroll_delta,
1131                },
1132                window,
1133                cx,
1134            );
1135        }
1136    }
1137
1138    fn update_visible_cursor(
1139        editor: &mut Editor,
1140        point: DisplayPoint,
1141        position_map: &PositionMap,
1142        window: &mut Window,
1143        cx: &mut Context<Editor>,
1144    ) {
1145        let snapshot = &position_map.snapshot;
1146        let Some(hub) = editor.collaboration_hub() else {
1147            return;
1148        };
1149        let start = snapshot.display_snapshot.clip_point(
1150            DisplayPoint::new(point.row(), point.column().saturating_sub(1)),
1151            Bias::Left,
1152        );
1153        let end = snapshot.display_snapshot.clip_point(
1154            DisplayPoint::new(
1155                point.row(),
1156                (point.column() + 1).min(snapshot.line_len(point.row())),
1157            ),
1158            Bias::Right,
1159        );
1160
1161        let range = snapshot
1162            .buffer_snapshot()
1163            .anchor_before(start.to_point(&snapshot.display_snapshot))
1164            ..snapshot
1165                .buffer_snapshot()
1166                .anchor_after(end.to_point(&snapshot.display_snapshot));
1167
1168        let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
1169            return;
1170        };
1171        let key = HoveredCursor {
1172            replica_id: selection.replica_id,
1173            selection_id: selection.selection.id,
1174        };
1175        editor.hovered_cursors.insert(
1176            key.clone(),
1177            cx.spawn_in(window, async move |editor, cx| {
1178                cx.background_executor().timer(CURSORS_VISIBLE_FOR).await;
1179                editor
1180                    .update(cx, |editor, cx| {
1181                        editor.hovered_cursors.remove(&key);
1182                        cx.notify();
1183                    })
1184                    .ok();
1185            }),
1186        );
1187        cx.notify()
1188    }
1189}
1190
1191fn scale_vertical_mouse_autoscroll_delta(delta: Pixels) -> f32 {
1192    (delta.pow(1.2) / 100.0).min(px(3.0)).into()
1193}
1194
1195fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 {
1196    (delta.pow(1.2) / 300.0).into()
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201    use super::*;
1202    use crate::{
1203        SelectionEffects, editor_tests::init_test, scroll::Autoscroll,
1204        test::editor_test_context::EditorTestContext,
1205    };
1206    use gpui::{Modifiers, TestAppContext};
1207
1208    #[gpui::test]
1209    async fn test_mouse_drag_preserves_pending_sticky_header_autoscroll(cx: &mut TestAppContext) {
1210        init_test(cx, |_| {});
1211        let mut cx = EditorTestContext::new(cx).await;
1212
1213        let line_height = cx.update_editor(|editor, window, cx| {
1214            editor
1215                .style(cx)
1216                .text
1217                .line_height_in_pixels(window.rem_size())
1218        });
1219
1220        let buffer = indoc::indoc! {"
1221                ˇfn foo() {
1222                    let abc = 123;
1223                }
1224                struct Bar;
1225                impl Bar {
1226                    fn new() -> Self {
1227                        Self
1228                    }
1229                }
1230                fn baz() {
1231                }
1232            "};
1233        cx.set_state(&buffer);
1234
1235        let text_origin_x = cx.update_editor(|editor, _, _| {
1236            editor
1237                .last_position_map
1238                .as_ref()
1239                .unwrap()
1240                .text_hitbox
1241                .bounds
1242                .origin
1243                .x
1244        });
1245
1246        cx.update_editor(|editor, window, cx| {
1247            editor.scroll(gpui::Point { x: 0., y: 5.5 }, None, window, cx);
1248        });
1249        cx.run_until_parked();
1250
1251        let mouse_drag_position = gpui::Point {
1252            x: text_origin_x,
1253            y: 2.25 * line_height,
1254        };
1255        cx.update_editor(|editor, window, cx| {
1256            let position_map = editor.last_position_map.as_ref().unwrap().clone();
1257            let anchor = editor
1258                .snapshot(window, cx)
1259                .display_snapshot
1260                .display_point_to_anchor(DisplayPoint::new(DisplayRow(5), 0), Bias::Left);
1261
1262            editor.change_selections(
1263                SelectionEffects::scroll(Autoscroll::top_relative(1.0)),
1264                window,
1265                cx,
1266                |selections| {
1267                    selections.clear_disjoint();
1268                    selections
1269                        .set_pending_anchor_range(anchor..anchor, crate::SelectMode::Character);
1270                },
1271            );
1272            assert!(editor.has_autoscroll_request());
1273
1274            EditorElement::mouse_dragged(
1275                editor,
1276                &MouseMoveEvent {
1277                    position: mouse_drag_position,
1278                    modifiers: Modifiers::none(),
1279                    pressed_button: Some(MouseButton::Left),
1280                },
1281                &position_map,
1282                window,
1283                cx,
1284            );
1285            assert!(editor.has_autoscroll_request());
1286        });
1287    }
1288}
1289
Served at tenant.openagents/omega Member data and write actions are omitted.