Skip to repository content1289 lines · 50.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:44:46.681Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
mouse.rs
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(¤t_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