1use std::{
2 collections::HashMap,
3 ops::Range,
4 rc::Rc,
5 sync::atomic::{AtomicU64, Ordering},
6};
7
8use gpui::{
9 App, AppContext as _, Bounds, Context, Element, ElementId, Entity, EntityId, EventEmitter,
10 Global, GlobalElementId, Half, Hitbox, InputEvent as _, InspectorElementId, IntoElement,
11 LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
12 ScrollDelta, ScrollWheelEvent, SharedString, Style, Subscription, TextLayout, WeakEntity,
13 Window, point, px,
14};
15
16use crate::text_boundary::{line_range_at, word_range_at};
17use crate::{AutoScroll, GlobalState};
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub struct TextSelectionScopeId(u64);
22
23impl TextSelectionScopeId {
24 pub fn new() -> Self {
29 static NEXT_SCOPE_ID: AtomicU64 = AtomicU64::new(1);
30 let value = NEXT_SCOPE_ID
31 .try_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
32 value.checked_add(1)
33 })
34 .expect("text selection scope identifiers exhausted");
35 Self(value)
36 }
37
38 #[cfg(test)]
39 const fn from_raw(value: u64) -> Self {
40 Self(value)
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct TextSelectionContentKey(u64);
47
48impl TextSelectionContentKey {
49 pub const fn new(value: u64) -> Self {
51 Self(value)
52 }
53
54 pub const fn value(self) -> u64 {
56 self.0
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct TextSelectionEndpoint {
63 entity_id: Option<EntityId>,
64 point: Point<Pixels>,
65 content_key: Option<TextSelectionContentKey>,
66}
67
68impl TextSelectionEndpoint {
69 pub(crate) const fn new(entity_id: Option<EntityId>, point: Point<Pixels>) -> Self {
71 Self {
72 entity_id,
73 point,
74 content_key: None,
75 }
76 }
77
78 pub(crate) const fn with_content_key(mut self, content_key: TextSelectionContentKey) -> Self {
80 self.content_key = Some(content_key);
81 self
82 }
83
84 pub const fn entity_id(&self) -> Option<EntityId> {
86 self.entity_id
87 }
88
89 pub const fn content_point(&self) -> Point<Pixels> {
91 self.point
92 }
93
94 pub const fn content_key(&self) -> Option<TextSelectionContentKey> {
96 self.content_key
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct TextSelectionWindowPoints {
103 anchor: Point<Pixels>,
104 cursor: Point<Pixels>,
105}
106
107impl TextSelectionWindowPoints {
108 pub const fn anchor(&self) -> Point<Pixels> {
110 self.anchor
111 }
112
113 pub const fn cursor(&self) -> Point<Pixels> {
115 self.cursor
116 }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct TextSelectionSnapshot {
122 anchor: TextSelectionEndpoint,
123 cursor: TextSelectionEndpoint,
124 is_selecting: bool,
125 window_points: Option<TextSelectionWindowPoints>,
126 coverage: TextSelectionCoverage,
127}
128
129#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub enum TextSelectionCoverage {
132 #[default]
134 Bounded,
135 FromStart,
137 ToEnd,
139 Full,
141}
142
143impl TextSelectionSnapshot {
144 pub(crate) const fn new(anchor: TextSelectionEndpoint, cursor: TextSelectionEndpoint) -> Self {
146 Self {
147 anchor,
148 cursor,
149 is_selecting: false,
150 window_points: None,
151 coverage: TextSelectionCoverage::Bounded,
152 }
153 }
154
155 pub(crate) const fn with_selecting(mut self, is_selecting: bool) -> Self {
157 self.is_selecting = is_selecting;
158 self
159 }
160
161 pub(crate) const fn with_window_points(
163 mut self,
164 window_points: Option<TextSelectionWindowPoints>,
165 ) -> Self {
166 self.window_points = window_points;
167 self
168 }
169
170 #[cfg(test)]
172 pub(crate) const fn with_coverage(mut self, coverage: TextSelectionCoverage) -> Self {
173 self.coverage = coverage;
174 self
175 }
176
177 pub const fn anchor(&self) -> TextSelectionEndpoint {
179 self.anchor
180 }
181
182 pub const fn cursor(&self) -> TextSelectionEndpoint {
184 self.cursor
185 }
186
187 pub const fn is_selecting(&self) -> bool {
189 self.is_selecting
190 }
191
192 pub const fn window_points(&self) -> Option<TextSelectionWindowPoints> {
194 self.window_points
195 }
196
197 pub const fn coverage(&self) -> TextSelectionCoverage {
199 self.coverage
200 }
201}
202
203pub struct TextSelectionRegistration {
205 hitbox: Hitbox,
206 bounds: Bounds<Pixels>,
207 scroll_offset: Point<Pixels>,
208 scope: TextSelectionScopeId,
209 document_order: u64,
210 text_bounds: Vec<Bounds<Pixels>>,
211 self_scroll: bool,
212}
213
214impl TextSelectionRegistration {
215 pub fn new(hitbox: Hitbox, bounds: Bounds<Pixels>) -> Self {
217 Self {
218 hitbox,
219 bounds,
220 scroll_offset: Point::default(),
221 scope: TextSelectionScopeId::default(),
222 document_order: 0,
223 text_bounds: Vec::new(),
224 self_scroll: false,
225 }
226 }
227
228 pub(crate) fn with_self_scroll(mut self, self_scroll: bool) -> Self {
233 self.self_scroll = self_scroll;
234 self
235 }
236
237 pub fn with_scroll_offset(mut self, scroll_offset: Point<Pixels>) -> Self {
239 self.scroll_offset = scroll_offset;
240 self
241 }
242
243 pub fn with_scope(mut self, scope: TextSelectionScopeId) -> Self {
245 self.scope = scope;
246 self
247 }
248
249 pub fn with_document_order(mut self, document_order: u64) -> Self {
251 self.document_order = document_order;
252 self
253 }
254
255 pub fn with_text_bounds(mut self, text_bounds: Vec<Bounds<Pixels>>) -> Self {
257 self.text_bounds = text_bounds;
258 self
259 }
260
261 pub fn hitbox(&self) -> &Hitbox {
263 &self.hitbox
264 }
265
266 pub const fn bounds(&self) -> Bounds<Pixels> {
268 self.bounds
269 }
270
271 pub const fn scroll_offset(&self) -> Point<Pixels> {
273 self.scroll_offset
274 }
275
276 pub const fn scope(&self) -> TextSelectionScopeId {
278 self.scope
279 }
280
281 pub const fn document_order(&self) -> u64 {
283 self.document_order
284 }
285
286 pub fn text_bounds(&self) -> &[Bounds<Pixels>] {
288 &self.text_bounds
289 }
290}
291
292#[derive(Clone)]
294pub struct TextSelectionRun {
295 document_order: u64,
297 text: SharedString,
299 layout: TextLayout,
301 bounds: Bounds<Pixels>,
303}
304
305impl TextSelectionRun {
306 pub fn new(text: impl Into<SharedString>, layout: TextLayout, bounds: Bounds<Pixels>) -> Self {
308 Self {
309 document_order: 0,
310 text: text.into(),
311 layout,
312 bounds,
313 }
314 }
315
316 pub const fn with_document_order(mut self, document_order: u64) -> Self {
318 self.document_order = document_order;
319 self
320 }
321
322 pub const fn document_order(&self) -> u64 {
324 self.document_order
325 }
326
327 pub fn text(&self) -> &SharedString {
329 &self.text
330 }
331
332 pub fn layout(&self) -> &TextLayout {
334 &self.layout
335 }
336
337 pub const fn bounds(&self) -> Bounds<Pixels> {
339 self.bounds
340 }
341}
342
343#[derive(Clone, Debug, Default, PartialEq, Eq)]
345pub struct TextSelectionProjection {
346 ranges: Vec<Option<Range<usize>>>,
348 is_active: bool,
350}
351
352impl TextSelectionProjection {
353 pub fn ranges(&self) -> &[Option<Range<usize>>] {
355 &self.ranges
356 }
357
358 pub const fn is_active(&self) -> bool {
360 self.is_active
361 }
362}
363
364fn project_ranges(
370 snapshot: Option<TextSelectionSnapshot>,
371 runs: &[TextSelectionRun],
372) -> TextSelectionProjection {
373 let Some(snapshot) = snapshot else {
374 return TextSelectionProjection {
375 ranges: vec![None; runs.len()],
376 is_active: false,
377 };
378 };
379 let Some(window_points) = snapshot.window_points() else {
380 return TextSelectionProjection {
381 ranges: vec![None; runs.len()],
382 is_active: true,
383 };
384 };
385
386 TextSelectionProjection {
387 ranges: runs
388 .iter()
389 .map(|run| selection_range_for_run(run, window_points.anchor, window_points.cursor))
390 .collect(),
391 is_active: true,
392 }
393}
394
395fn selection_range_for_run(
396 run: &TextSelectionRun,
397 selection_start: Point<Pixels>,
398 selection_end: Point<Pixels>,
399) -> Option<Range<usize>> {
400 if run.text.len() != run.layout.len() {
401 return None;
402 }
403
404 let line_height = run.layout.line_height();
405 let mut range = None;
406 for (offset, character) in run.text.char_indices() {
407 let next_offset = offset + character.len_utf8();
408 let Some(position) = run.layout.position_for_index(offset) else {
409 continue;
410 };
411
412 let char_width = run
413 .layout
414 .position_for_index(next_offset)
415 .filter(|next| next.y == position.y)
416 .map_or_else(|| line_height.half(), |next| next.x - position.x);
417
418 if point_in_selection_band(
419 position,
420 char_width,
421 selection_start,
422 selection_end,
423 line_height,
424 ) {
425 range.get_or_insert(offset..offset).end = next_offset;
426 }
427 }
428 range
429}
430
431fn points_for_multi_click(
432 runs: &[TextSelectionRun],
433 position: Point<Pixels>,
434 click_count: usize,
435) -> Option<(Point<Pixels>, Point<Pixels>)> {
436 let run = runs.iter().find(|run| run.bounds.contains(&position))?;
437 if run.text.len() != run.layout.len() {
438 return None;
439 }
440 let offset = run.layout.index_for_position(position).ok()?;
441 let range = match click_count {
442 2 => word_range_at(&run.text, offset)?,
443 3.. => line_range_at(&run.text, offset),
444 _ => return None,
445 };
446 if range.is_empty() {
447 return None;
448 }
449 Some((
450 run.layout.position_for_index(range.start)?,
451 run.layout.position_for_index(range.end)?,
452 ))
453}
454
455fn point_in_selection_band(
456 position: Point<Pixels>,
457 char_width: Pixels,
458 selection_start: Point<Pixels>,
459 selection_end: Point<Pixels>,
460 line_height: Pixels,
461) -> bool {
462 let point_in_line =
463 |point: Point<Pixels>| point.y >= position.y && point.y < position.y + line_height;
464 let top = selection_start.y.min(selection_end.y);
465 let bottom = selection_start.y.max(selection_end.y);
466 let x = position.x + char_width.half();
467
468 if position.y + line_height <= top || position.y > bottom {
469 return false;
470 }
471
472 if point_in_line(selection_start) && point_in_line(selection_end) {
473 let left = selection_start.x.min(selection_end.x);
474 let right = selection_start.x.max(selection_end.x);
475 return x >= left && x <= right;
476 }
477
478 let (top_point, bottom_point) = if selection_start.y < selection_end.y {
479 (selection_start, selection_end)
480 } else {
481 (selection_end, selection_start)
482 };
483 if point_in_line(top_point) {
484 x >= top_point.x
485 } else if point_in_line(bottom_point) {
486 x <= bottom_point.x
487 } else {
488 true
489 }
490}
491
492type FocusCallback = Rc<dyn Fn(&mut Window, &mut App)>;
493type ClearHandler = Rc<dyn Fn(&mut App)>;
494type CopyCallback = Rc<dyn Fn(&mut App) -> String>;
495type ContentKeyResolver = Rc<dyn Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey>>;
496
497#[derive(Clone, Copy, Debug, PartialEq)]
499pub enum TextSelectionEvent {
500 SelectionChanged(Option<TextSelectionSnapshot>),
502 AutoScroll(Option<Pixels>),
504 Cleared,
506}
507
508struct CopyItem {
509 document_order: u64,
510 callback: Option<CopyCallback>,
511 fallback: String,
512}
513
514fn resolve_copy_items(mut items: Vec<CopyItem>, cx: &mut App) -> String {
515 items.sort_by_key(|item| item.document_order);
516 items
517 .into_iter()
518 .map(|item| {
519 item.callback
520 .map(|callback| callback(cx))
521 .unwrap_or(item.fallback)
522 })
523 .filter(|text| !text.trim().is_empty())
524 .collect::<Vec<_>>()
525 .join("\n")
526}
527
528fn dispatch_clear_handlers(handlers: Vec<ClearHandler>, cx: &mut App) {
529 for handler in handlers {
530 handler(cx);
531 }
532}
533
534struct SelectableTextState {
535 fallback_copy_text: String,
536 projected_copy_text: Option<String>,
537 runs: Vec<TextSelectionRun>,
538 local_selection: bool,
539 snapshot: Option<TextSelectionSnapshot>,
540 on_focus: Option<FocusCallback>,
541 clear: Option<ClearHandler>,
542 copy: Option<CopyCallback>,
543 content_key_resolver: Option<ContentKeyResolver>,
544}
545
546impl EventEmitter<TextSelectionEvent> for SelectableTextState {}
547
548impl SelectableTextState {
549 fn new(fallback_copy_text: impl Into<String>) -> Self {
550 Self {
551 fallback_copy_text: fallback_copy_text.into(),
552 projected_copy_text: None,
553 runs: Vec::new(),
554 local_selection: false,
555 snapshot: None,
556 on_focus: None,
557 clear: None,
558 copy: None,
559 content_key_resolver: None,
560 }
561 }
562
563 fn snapshot(&self) -> Option<TextSelectionSnapshot> {
565 self.snapshot
566 }
567
568 fn set_fallback_copy_text(&mut self, text: impl Into<String>) {
570 self.fallback_copy_text = text.into();
571 self.projected_copy_text = None;
572 }
573
574 fn set_local_selection(&mut self, active: bool) {
576 self.local_selection = active;
577 }
578
579 fn update_runs(&mut self, runs: &[TextSelectionRun]) -> TextSelectionProjection {
586 self.runs = runs.to_vec();
587 let states = project_ranges(self.snapshot, runs);
588 let mut selected_runs = runs
589 .iter()
590 .zip(states.ranges())
591 .enumerate()
592 .filter_map(|(index, (run, state))| {
593 state.as_ref().map(|range| {
594 debug_assert!(run.text.is_char_boundary(range.start));
595 debug_assert!(run.text.is_char_boundary(range.end));
596 (
597 run.document_order,
598 index,
599 run.text[range.clone()].to_string(),
600 )
601 })
602 })
603 .collect::<Vec<_>>();
604 selected_runs.sort_by_key(|(order, index, _)| (*order, *index));
605 self.projected_copy_text =
606 Some(selected_runs.into_iter().map(|(_, _, text)| text).collect());
607 states
608 }
609
610 fn set_focus_handler(&mut self, callback: impl Fn(&mut Window, &mut App) + 'static) {
612 self.on_focus = Some(Rc::new(callback));
613 }
614
615 fn clear_with(&mut self, callback: impl Fn(&mut App) + 'static) {
616 self.clear = Some(Rc::new(callback));
617 }
618
619 fn copy_with(&mut self, callback: impl Fn(&mut App) -> String + 'static) {
621 self.copy = Some(Rc::new(callback));
622 }
623
624 fn resolve_content_key_with(
626 &mut self,
627 callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
628 ) {
629 self.content_key_resolver = Some(Rc::new(callback));
630 }
631
632 fn set_snapshot(&mut self, snapshot: Option<TextSelectionSnapshot>, cx: &mut Context<Self>) {
633 if self.snapshot == snapshot {
634 return;
635 }
636 self.snapshot = snapshot;
637 self.projected_copy_text = None;
638 cx.emit(TextSelectionEvent::SelectionChanged(snapshot));
639 }
640
641 fn clear_state(&mut self, cx: &mut Context<Self>) -> Option<ClearHandler> {
642 self.snapshot = None;
643 self.projected_copy_text = None;
644 self.local_selection = false;
645 cx.emit(TextSelectionEvent::Cleared);
646 cx.emit(TextSelectionEvent::SelectionChanged(None));
647 self.clear.clone()
648 }
649
650 fn set_auto_scroll(&self, delta: Option<Pixels>, cx: &mut Context<Self>) {
651 cx.emit(TextSelectionEvent::AutoScroll(delta));
652 }
653
654 fn focus(&self, window: &mut Window, cx: &mut App) {
655 if let Some(callback) = self.on_focus.clone() {
656 window.defer(cx, move |window, cx| callback(window, cx));
657 }
658 }
659
660 fn copy_item(&self, document_order: u64) -> Option<CopyItem> {
661 (self.snapshot.is_some() || self.local_selection).then(|| CopyItem {
662 document_order,
663 callback: self.copy.clone(),
664 fallback: self
665 .projected_copy_text
666 .clone()
667 .unwrap_or_else(|| self.fallback_copy_text.clone()),
668 })
669 }
670}
671
672#[derive(Clone)]
674pub struct TextSelectionHandle(Entity<SelectableTextState>);
675
676impl TextSelectionHandle {
677 pub fn new(fallback_copy_text: impl Into<String>, cx: &mut App) -> Self {
679 Self(cx.new(|_| SelectableTextState::new(fallback_copy_text)))
680 }
681
682 pub fn entity_id(&self) -> EntityId {
684 self.0.entity_id()
685 }
686
687 pub fn snapshot(&self, cx: &App) -> Option<TextSelectionSnapshot> {
689 self.0.read(cx).snapshot()
690 }
691
692 pub fn set_fallback_copy_text(&self, text: impl Into<String>, cx: &mut App) {
694 self.0
695 .update(cx, |state, _| state.set_fallback_copy_text(text));
696 }
697
698 pub fn set_local_selection(&self, active: bool, cx: &mut App) {
700 self.0
701 .update(cx, |state, _| state.set_local_selection(active));
702 }
703
704 pub fn has_local_selection(&self, cx: &App) -> bool {
706 self.0.read(cx).local_selection
707 }
708
709 pub fn register(
711 &self,
712 mut registration: TextSelectionRegistration,
713 window: &mut Window,
714 cx: &mut App,
715 ) {
716 if let Some(scope) = current_text_selection_scope(window.window_handle().window_id(), cx) {
717 registration.scope = scope;
718 }
719 let Some(state) = WindowSelectionState::existing(window, cx) else {
720 return;
721 };
722 state.update(cx, |state, cx| {
723 state.register_participant(self.clone(), registration, cx)
724 });
725 }
726
727 pub fn update_runs(&self, runs: &[TextSelectionRun], cx: &mut App) -> TextSelectionProjection {
729 self.0.update(cx, |state, _| state.update_runs(runs))
730 }
731
732 pub fn subscribe(
734 &self,
735 mut callback: impl FnMut(&TextSelectionEvent, &mut App) + 'static,
736 cx: &mut App,
737 ) -> Subscription {
738 cx.subscribe(&self.0, move |_, event, cx| callback(event, cx))
739 }
740
741 #[must_use = "retain the subscription or explicitly detach it"]
743 pub fn refresh_window_on_change(&self, window: &Window, cx: &mut App) -> Subscription {
744 let window = window.window_handle();
745 self.subscribe(
746 move |event, cx| {
747 if matches!(event, TextSelectionEvent::SelectionChanged(_)) {
748 _ = window.update(cx, |_, window, _| window.refresh());
749 }
750 },
751 cx,
752 )
753 }
754
755 pub fn focus_with(&self, callback: impl Fn(&mut Window, &mut App) + 'static, cx: &mut App) {
757 self.0
758 .update(cx, |state, _| state.set_focus_handler(callback));
759 }
760
761 pub fn clear_with(&self, callback: impl Fn(&mut App) + 'static, cx: &mut App) {
763 self.0.update(cx, |state, _| state.clear_with(callback));
764 }
765
766 pub fn copy_with(&self, callback: impl Fn(&mut App) -> String + 'static, cx: &mut App) {
768 self.0.update(cx, |state, _| state.copy_with(callback));
769 }
770
771 pub fn resolve_content_key_with(
773 &self,
774 callback: impl Fn(Point<Pixels>, &App) -> Option<TextSelectionContentKey> + 'static,
775 cx: &mut App,
776 ) {
777 self.0
778 .update(cx, |state, _| state.resolve_content_key_with(callback));
779 }
780
781 fn downgrade(&self) -> WeakEntity<SelectableTextState> {
782 self.0.downgrade()
783 }
784}
785
786#[derive(Clone)]
787struct ParticipantRegistration {
788 participant: WeakEntity<SelectableTextState>,
789 registration: Rc<TextSelectionRegistration>,
790 generation: u64,
791}
792
793#[derive(Clone)]
794struct SelectionEndpoint {
795 participant: Option<WeakEntity<SelectableTextState>>,
796 point: Point<Pixels>,
797 inside: bool,
798 inside_text: bool,
799 content_key: Option<TextSelectionContentKey>,
800 content_key_resolver: Option<(ContentKeyResolver, Point<Pixels>)>,
801}
802
803impl SelectionEndpoint {
804 fn snapshot(&self) -> TextSelectionEndpoint {
805 let snapshot = TextSelectionEndpoint::new(self.entity_id(), self.point);
806 if let Some(content_key) = self.content_key {
807 snapshot.with_content_key(content_key)
808 } else {
809 snapshot
810 }
811 }
812
813 fn resolve(
814 &self,
815 participants: &HashMap<EntityId, ParticipantRegistration>,
816 ) -> Option<Point<Pixels>> {
817 let participant = self.participant.as_ref()?;
818 let registration = participants.get(&participant.entity_id())?;
819 participant.upgrade()?;
820 Some(
821 self.point
822 + registration.registration.scroll_offset
823 + registration.registration.bounds.origin,
824 )
825 }
826
827 fn entity_id(&self) -> Option<EntityId> {
828 self.participant
829 .as_ref()
830 .map(|participant| participant.entity_id())
831 }
832}
833
834#[derive(Default)]
836struct WindowSelectionState {
837 participants: HashMap<EntityId, ParticipantRegistration>,
838 active_scope: TextSelectionScopeId,
839 anchor: Option<SelectionEndpoint>,
840 cursor: Option<SelectionEndpoint>,
841 pending_extension_anchor: Option<SelectionEndpoint>,
842 is_selecting: bool,
843 did_hit_text: bool,
844 frame_generation: u64,
845 finish_frame_scheduled: bool,
846 refresh_held_cursor: bool,
847 mouse_down_prepared: bool,
848 auto_scroll: AutoScroll,
849}
850
851impl WindowSelectionState {
852 fn resolve_content_keys(state: &Entity<Self>, cx: &mut App) {
853 let pending = state.update(cx, |state, _| {
854 [
855 state
856 .anchor
857 .as_ref()
858 .and_then(|endpoint| endpoint.content_key_resolver.clone()),
859 state
860 .cursor
861 .as_ref()
862 .and_then(|endpoint| endpoint.content_key_resolver.clone()),
863 ]
864 });
865 let resolved =
866 pending.map(|pending| pending.and_then(|(callback, point)| callback(point, cx)));
867 state.update(cx, |state, cx| {
868 if let (Some(endpoint), Some(key)) = (state.anchor.as_mut(), resolved[0]) {
869 endpoint.content_key = Some(key);
870 endpoint.content_key_resolver = None;
871 }
872 if let (Some(endpoint), Some(key)) = (state.cursor.as_mut(), resolved[1]) {
873 endpoint.content_key = Some(key);
874 endpoint.content_key_resolver = None;
875 }
876 state.publish_snapshots(cx);
877 });
878 }
879 fn acquire(window_id: gpui::WindowId, cx: &mut App) -> Entity<Self> {
880 if !cx.has_global::<SelectionStateRegistry>() {
881 cx.set_global(SelectionStateRegistry::default());
882 }
883 if let Some(state) = cx
884 .global::<SelectionStateRegistry>()
885 .0
886 .get(&window_id)
887 .and_then(WeakEntity::upgrade)
888 {
889 return state;
890 }
891
892 let active_scope = if cx.has_global::<PendingTextSelectionScopes>() {
893 cx.global_mut::<PendingTextSelectionScopes>()
894 .0
895 .remove(&window_id)
896 .unwrap_or_default()
897 } else {
898 TextSelectionScopeId::default()
899 };
900
901 let state = cx.new(move |cx| {
902 let entity_id = cx.entity_id();
903 cx.on_release(move |state: &mut WindowSelectionState, cx| {
904 let handlers = state.clear_state(cx);
905 if cx.has_global::<SelectionStateRegistry>() {
906 let registry = &mut cx.global_mut::<SelectionStateRegistry>().0;
907 if registry
908 .get(&window_id)
909 .is_some_and(|state| state.entity_id() == entity_id)
910 {
911 registry.remove(&window_id);
912 }
913 }
914 if !handlers.is_empty() {
915 cx.defer(move |cx| dispatch_clear_handlers(handlers, cx));
916 }
917 })
918 .detach();
919 Self {
920 active_scope,
921 ..Self::default()
922 }
923 });
924 cx.global_mut::<SelectionStateRegistry>()
925 .0
926 .insert(window_id, state.downgrade());
927 state
928 }
929
930 #[cfg(test)]
931 fn ensure(window: &Window, cx: &mut App) -> Entity<Self> {
932 Self::acquire(window.window_handle().window_id(), cx)
933 }
934
935 fn existing(window: &Window, cx: &App) -> Option<Entity<Self>> {
936 if !cx.has_global::<SelectionStateRegistry>() {
937 return None;
938 }
939 cx.global::<SelectionStateRegistry>()
940 .0
941 .get(&window.window_handle().window_id())
942 .and_then(WeakEntity::upgrade)
943 }
944
945 #[cfg(test)]
947 fn set_active_scope(&mut self, scope: TextSelectionScopeId, cx: &mut App) {
948 let handlers = self.set_active_scope_state(scope, cx);
949 dispatch_clear_handlers(handlers, cx);
950 }
951
952 fn set_active_scope_state(
953 &mut self,
954 scope: TextSelectionScopeId,
955 cx: &mut App,
956 ) -> Vec<ClearHandler> {
957 if self.active_scope == scope {
958 return Vec::new();
959 }
960 let handlers = self.clear_state(cx);
961 self.active_scope = scope;
962 self.publish_snapshots(cx);
963 handlers
964 }
965
966 pub fn finish_frame(&mut self, cx: &mut App) -> Vec<ClearHandler> {
972 self.finish_frame_scheduled = false;
973 let stale = self
974 .participants
975 .iter()
976 .filter_map(|(id, registration)| {
977 (registration.generation != self.frame_generation)
978 .then(|| (*id, registration.participant.clone()))
979 })
980 .collect::<Vec<_>>();
981 let mut handlers = Vec::new();
982 for (id, participant) in stale {
983 self.participants.remove(&id);
984 if let Some(participant) = participant.upgrade() {
985 if let Some(handler) = participant.update(cx, |state, cx| state.clear_state(cx)) {
986 handlers.push(handler);
987 }
988 }
989 }
990 self.publish_snapshots(cx);
991 self.frame_generation = self.frame_generation.wrapping_add(1);
992 handlers
993 }
994
995 fn schedule_finish_frame(&mut self) -> bool {
996 if self.finish_frame_scheduled {
997 return false;
998 }
999 self.finish_frame_scheduled = true;
1000 true
1001 }
1002
1003 pub fn register_participant(
1005 &mut self,
1006 selection: TextSelectionHandle,
1007 registration: TextSelectionRegistration,
1008 cx: &mut App,
1009 ) {
1010 self.prune_dead_participants();
1011 if self.is_selecting
1012 && registration.self_scroll
1013 && self.anchor.as_ref().and_then(SelectionEndpoint::entity_id)
1014 == Some(selection.entity_id())
1015 && self
1016 .participants
1017 .get(&selection.entity_id())
1018 .is_some_and(|previous| {
1019 previous.registration.scroll_offset != registration.scroll_offset
1020 || previous.registration.bounds != registration.bounds
1021 })
1022 {
1023 self.refresh_held_cursor = true;
1024 }
1025 self.participants.insert(
1026 selection.entity_id(),
1027 ParticipantRegistration {
1028 participant: selection.downgrade(),
1029 registration: Rc::new(registration),
1030 generation: self.frame_generation,
1031 },
1032 );
1033 self.publish_snapshots(cx);
1034 }
1035
1036 #[cfg(test)]
1038 fn begin(&mut self, position: Point<Pixels>, extend: bool, cx: &mut App) {
1039 self.begin_impl(position, extend, false, None, cx);
1040 }
1041
1042 #[cfg(test)]
1044 fn update(&mut self, position: Point<Pixels>, cx: &mut App) {
1045 self.update_impl(position, None, cx);
1046 }
1047
1048 pub fn end(&mut self, cx: &mut App) {
1050 self.pending_extension_anchor = None;
1051 if !self.is_selecting {
1052 return;
1053 }
1054 self.is_selecting = false;
1055 if !self.did_hit_text {
1056 self.anchor = None;
1057 self.cursor = None;
1058 }
1059 self.stop_anchor_auto_scroll(cx);
1060 self.publish_snapshots(cx);
1061 }
1062
1063 pub fn clear(&mut self, cx: &mut App) {
1065 let handlers = self.clear_state(cx);
1066 dispatch_clear_handlers(handlers, cx);
1067 }
1068
1069 fn clear_state(&mut self, cx: &mut App) -> Vec<ClearHandler> {
1070 self.stop_anchor_auto_scroll(cx);
1071 self.anchor = None;
1072 self.cursor = None;
1073 self.pending_extension_anchor = None;
1074 self.is_selecting = false;
1075 self.did_hit_text = false;
1076 self.prune_dead_participants();
1077 self.participants
1078 .values()
1079 .filter_map(|registration| registration.participant.upgrade())
1080 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1081 .collect()
1082 }
1083
1084 fn copy_items(&self, cx: &App) -> Vec<CopyItem> {
1085 self.participants
1086 .values()
1087 .filter_map(|registration| {
1088 let participant = registration.participant.upgrade()?;
1089 participant
1090 .read(cx)
1091 .copy_item(registration.registration.document_order)
1092 })
1093 .collect()
1094 }
1095
1096 #[cfg(test)]
1097 fn selected_text(&self, cx: &mut App) -> String {
1098 resolve_copy_items(self.copy_items(cx), cx)
1099 }
1100
1101 pub fn has_selection(&self, cx: &App) -> bool {
1103 self.snapshot().is_some()
1104 || self.participants.values().any(|registration| {
1105 registration
1106 .participant
1107 .upgrade()
1108 .is_some_and(|participant| participant.read(cx).local_selection)
1109 })
1110 }
1111
1112 pub fn snapshot(&self) -> Option<TextSelectionSnapshot> {
1114 if !self.did_hit_text {
1115 return None;
1116 }
1117 let anchor_endpoint = self.anchor.as_ref()?;
1118 let cursor_endpoint = self.cursor.as_ref()?;
1119 let anchor = anchor_endpoint.resolve(&self.participants)?;
1120 let cursor = cursor_endpoint.resolve(&self.participants)?;
1121 (anchor != cursor).then(|| {
1122 TextSelectionSnapshot::new(anchor_endpoint.snapshot(), cursor_endpoint.snapshot())
1123 .with_selecting(self.is_selecting)
1124 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
1125 })
1126 }
1127
1128 #[cfg(test)]
1130 fn is_selecting(&self) -> bool {
1131 self.is_selecting
1132 }
1133
1134 fn prepare_for_mouse_down(&mut self, extend: bool, cx: &mut App) -> Vec<ClearHandler> {
1135 let pending_extension_anchor = extend.then(|| self.anchor.clone()).flatten();
1136 self.stop_anchor_auto_scroll(cx);
1137 self.anchor = None;
1138 self.cursor = None;
1139 self.pending_extension_anchor = None;
1140 self.is_selecting = false;
1141 self.did_hit_text = false;
1142 self.prune_dead_participants();
1143 let handlers = self
1144 .participants
1145 .values()
1146 .filter_map(|registration| registration.participant.upgrade())
1147 .filter_map(|participant| participant.update(cx, |state, cx| state.clear_state(cx)))
1148 .collect();
1149 self.pending_extension_anchor = pending_extension_anchor;
1150 handlers
1151 }
1152
1153 fn begin_in_window(
1154 &mut self,
1155 position: Point<Pixels>,
1156 extend: bool,
1157 window: &mut Window,
1158 cx: &mut App,
1159 ) {
1160 self.begin_impl(position, extend, true, Some(window), cx);
1161 }
1162
1163 fn update_in_window(
1164 &mut self,
1165 position: Point<Pixels>,
1166 window: &Window,
1167 cx: &mut Context<Self>,
1168 ) {
1169 if !cx.has_active_drag() {
1170 self.update_impl(position, Some(window), cx);
1171 self.update_auto_scroll(position, window, cx);
1172 }
1173 }
1174
1175 fn select_at(
1176 &mut self,
1177 position: Point<Pixels>,
1178 click_count: usize,
1179 window: &mut Window,
1180 cx: &mut App,
1181 ) {
1182 GlobalState::init(cx);
1183 if GlobalState::is_text_selection_suppressed(cx) {
1184 return;
1185 }
1186 let hit = self.endpoint(position, Some(window), cx);
1187 if !hit.inside_text {
1188 return;
1189 }
1190 let Some(participant) = hit
1191 .participant
1192 .and_then(|participant| participant.upgrade())
1193 else {
1194 return;
1195 };
1196 let points = points_for_multi_click(&participant.read(cx).runs, position, click_count);
1197 let Some((anchor, cursor)) = points else {
1198 return;
1199 };
1200 let Some(registration) = self.participants.get(&participant.entity_id()) else {
1201 return;
1202 };
1203 let content_key_resolver = participant.read(cx).content_key_resolver.clone();
1204 let to_endpoint = |point: Point<Pixels>| {
1205 let content_point = point
1206 - registration.registration.bounds.origin
1207 - registration.registration.scroll_offset;
1208 SelectionEndpoint {
1209 participant: Some(participant.downgrade()),
1210 point: content_point,
1211 inside: true,
1212 inside_text: true,
1213 content_key: None,
1214 content_key_resolver: content_key_resolver
1215 .clone()
1216 .map(|resolver| (resolver, content_point)),
1217 }
1218 };
1219 self.anchor = Some(to_endpoint(anchor));
1220 self.cursor = Some(to_endpoint(cursor));
1221 self.did_hit_text = true;
1222 self.is_selecting = false;
1223 participant.update(cx, |state, cx| state.focus(window, cx));
1224 self.publish_snapshots(cx);
1225 }
1226
1227 #[cfg(test)]
1228 fn update_in_window_with_active_drag(
1229 &mut self,
1230 position: Point<Pixels>,
1231 active_drag: bool,
1232 window: &Window,
1233 cx: &mut App,
1234 ) {
1235 if !active_drag {
1236 self.update_impl(position, Some(window), cx);
1237 }
1238 }
1239
1240 fn begin_impl(
1241 &mut self,
1242 position: Point<Pixels>,
1243 extend: bool,
1244 already_prepared: bool,
1245 window: Option<&mut Window>,
1246 cx: &mut App,
1247 ) {
1248 GlobalState::init(cx);
1249 if GlobalState::is_text_selection_suppressed(cx) {
1250 self.pending_extension_anchor = None;
1251 return;
1252 }
1253 let previous_anchor = extend
1254 .then(|| {
1255 self.pending_extension_anchor
1256 .take()
1257 .or_else(|| self.anchor.clone())
1258 })
1259 .flatten()
1260 .filter(|anchor| anchor.resolve(&self.participants).is_some());
1261 if !extend && !already_prepared {
1262 self.clear(cx);
1263 }
1264 let endpoint = self.endpoint(position, window.as_deref(), cx);
1265 let focus_participant = endpoint
1266 .inside
1267 .then(|| endpoint.participant.clone())
1268 .flatten();
1269 let anchor = previous_anchor.unwrap_or_else(|| endpoint.clone());
1270 self.anchor = Some(anchor.clone());
1271 self.cursor = Some(endpoint.clone());
1272 self.did_hit_text = anchor.inside_text || endpoint.inside_text;
1273 self.is_selecting = true;
1274 if let Some(participant) = focus_participant.and_then(|participant| participant.upgrade()) {
1275 if let Some(window) = window {
1276 participant.update(cx, |state, cx| state.focus(window, cx));
1277 }
1278 }
1279 self.publish_snapshots(cx);
1280 }
1281
1282 fn update_impl(&mut self, position: Point<Pixels>, window: Option<&Window>, cx: &mut App) {
1283 if !self.is_selecting {
1284 return;
1285 }
1286 let endpoint = self.endpoint(position, window, cx);
1287 self.did_hit_text |= endpoint.inside_text;
1288 self.cursor = Some(endpoint);
1289 if window.is_none() {
1290 self.update_participant_auto_scroll(position, cx);
1291 }
1292 self.publish_snapshots(cx);
1293 }
1294
1295 fn endpoint(
1296 &mut self,
1297 position: Point<Pixels>,
1298 window: Option<&Window>,
1299 cx: &App,
1300 ) -> SelectionEndpoint {
1301 self.prune_dead_participants();
1302 let mut hit: Option<(
1303 WeakEntity<SelectableTextState>,
1304 Rc<TextSelectionRegistration>,
1305 f32,
1306 )> = None;
1307 let mut predecessor: Option<(
1308 WeakEntity<SelectableTextState>,
1309 Rc<TextSelectionRegistration>,
1310 )> = None;
1311 let mut first: Option<(
1312 WeakEntity<SelectableTextState>,
1313 Rc<TextSelectionRegistration>,
1314 )> = None;
1315
1316 for registration in self.participants.values() {
1317 if registration.registration.scope != self.active_scope
1318 || registration.participant.upgrade().is_none()
1319 {
1320 continue;
1321 }
1322 let participant_geometry = ®istration.registration;
1323 let hovered = window.map_or_else(
1324 || participant_geometry.bounds.contains(&position),
1325 |window| participant_geometry.hitbox.is_hovered(window),
1326 );
1327 if hovered {
1328 let area = f32::from(participant_geometry.bounds.size.width)
1329 * f32::from(participant_geometry.bounds.size.height);
1330 if hit.as_ref().is_none_or(|(_, best, best_area)| {
1331 area < *best_area
1332 || (area == *best_area
1333 && participant_geometry.document_order < best.document_order)
1334 }) {
1335 hit = Some((
1336 registration.participant.clone(),
1337 participant_geometry.clone(),
1338 area,
1339 ));
1340 }
1341 }
1342 if participant_geometry.bounds.top() <= position.y
1343 && predecessor.as_ref().is_none_or(|(_, best)| {
1344 participant_geometry.bounds.top() > best.bounds.top()
1345 || (participant_geometry.bounds.top() == best.bounds.top()
1346 && participant_geometry.document_order < best.document_order)
1347 })
1348 {
1349 predecessor = Some((
1350 registration.participant.clone(),
1351 participant_geometry.clone(),
1352 ));
1353 }
1354 if first.as_ref().is_none_or(|(_, best)| {
1355 participant_geometry.bounds.top() < best.bounds.top()
1356 || (participant_geometry.bounds.top() == best.bounds.top()
1357 && participant_geometry.document_order < best.document_order)
1358 }) {
1359 first = Some((
1360 registration.participant.clone(),
1361 participant_geometry.clone(),
1362 ));
1363 }
1364 }
1365
1366 let selection = hit
1367 .map(|(participant, registration, _)| (participant, registration, true))
1368 .or_else(|| {
1369 predecessor
1370 .or(first)
1371 .map(|(participant, registration)| (participant, registration, false))
1372 });
1373 match selection {
1374 Some((participant, registration, inside)) => {
1375 let point = position - registration.bounds.origin - registration.scroll_offset;
1376 let content_key_resolver = participant.upgrade().and_then(|participant| {
1377 participant
1378 .read(cx)
1379 .content_key_resolver
1380 .clone()
1381 .map(|callback| (callback, point))
1382 });
1383 SelectionEndpoint {
1384 point,
1385 participant: Some(participant),
1386 inside,
1387 inside_text: inside
1388 && registration
1389 .text_bounds
1390 .iter()
1391 .any(|bounds| bounds.contains(&position)),
1392 content_key: None,
1393 content_key_resolver,
1394 }
1395 }
1396 None => SelectionEndpoint {
1397 participant: None,
1398 point: position,
1399 inside: false,
1400 inside_text: false,
1401 content_key: None,
1402 content_key_resolver: None,
1403 },
1404 }
1405 }
1406
1407 fn publish_snapshots(&mut self, cx: &mut App) {
1408 self.prune_dead_participants();
1409 let snapshot = self.snapshot();
1410 let single_participant = self.single_participant();
1411 for (id, registration) in &self.participants {
1412 let Some(participant) = registration.participant.upgrade() else {
1413 continue;
1414 };
1415 let participant_snapshot = (registration.registration.scope == self.active_scope
1416 && self.participates(*id, registration)
1417 && single_participant.is_none_or(|single| single == *id))
1418 .then_some(snapshot)
1419 .flatten()
1420 .map(|mut snapshot| {
1421 snapshot.coverage = self.coverage_for(*id);
1422 snapshot
1423 });
1424 participant.update(cx, |state, cx| state.set_snapshot(participant_snapshot, cx));
1425 }
1426 }
1427
1428 fn coverage_for(&self, id: EntityId) -> TextSelectionCoverage {
1429 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1430 return TextSelectionCoverage::Bounded;
1431 };
1432 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1433 return TextSelectionCoverage::Bounded;
1434 };
1435 if anchor == cursor {
1436 return TextSelectionCoverage::Bounded;
1437 }
1438 let anchor_order = self.participants[&anchor].registration.document_order;
1439 let cursor_order = self.participants[&cursor].registration.document_order;
1440 if id != anchor && id != cursor {
1441 TextSelectionCoverage::Full
1442 } else if (id == anchor) == (anchor_order < cursor_order) {
1443 TextSelectionCoverage::ToEnd
1444 } else {
1445 TextSelectionCoverage::FromStart
1446 }
1447 }
1448
1449 fn single_participant(&self) -> Option<EntityId> {
1450 let anchor = self.anchor.as_ref()?.entity_id()?;
1451 let cursor = self.cursor.as_ref()?.entity_id()?;
1452 (anchor == cursor).then_some(anchor)
1453 }
1454
1455 fn participates(&self, id: EntityId, registration: &ParticipantRegistration) -> bool {
1456 let Some(anchor) = self.anchor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1457 return false;
1458 };
1459 let Some(cursor) = self.cursor.as_ref().and_then(SelectionEndpoint::entity_id) else {
1460 return false;
1461 };
1462 let Some(anchor_registration) = self.participants.get(&anchor) else {
1463 return false;
1464 };
1465 let Some(cursor_registration) = self.participants.get(&cursor) else {
1466 return false;
1467 };
1468 let start = anchor_registration
1469 .registration
1470 .document_order
1471 .min(cursor_registration.registration.document_order);
1472 let end = anchor_registration
1473 .registration
1474 .document_order
1475 .max(cursor_registration.registration.document_order);
1476 (start..=end).contains(®istration.registration.document_order)
1477 || id == anchor
1478 || id == cursor
1479 }
1480
1481 fn update_auto_scroll(
1482 &mut self,
1483 position: Point<Pixels>,
1484 window: &Window,
1485 cx: &mut Context<Self>,
1486 ) {
1487 if !self.is_selecting {
1490 return;
1491 }
1492 let Some((_, registration)) = self.anchor_registration() else {
1493 return;
1494 };
1495 if registration.self_scroll {
1498 self.auto_scroll.stop();
1499 self.update_participant_auto_scroll(position, cx);
1500 return;
1501 }
1502 let visible_bounds = registration.hitbox.content_mask.bounds;
1507 const HIT_TEST_INSET: Pixels = px(1.);
1509 if visible_bounds.size.width < HIT_TEST_INSET * 2.
1511 || visible_bounds.size.height < HIT_TEST_INSET * 2.
1512 {
1513 self.stop_anchor_auto_scroll(cx);
1514 return;
1515 }
1516 let delta = AutoScroll::compute_delta(position.y, visible_bounds);
1517 let event_position = point(
1518 position.x.clamp(
1519 visible_bounds.left() + HIT_TEST_INSET,
1520 visible_bounds.right() - HIT_TEST_INSET,
1521 ),
1522 position.y.clamp(
1523 visible_bounds.top() + HIT_TEST_INSET,
1524 visible_bounds.bottom() - HIT_TEST_INSET,
1525 ),
1526 );
1527 self.auto_scroll.last_drag_position = Some(event_position);
1528 let window = window.window_handle();
1529 self.auto_scroll.set(delta, cx, move |delta, state, cx| {
1530 let Some(position) = state.auto_scroll.last_drag_position else {
1531 return;
1532 };
1533 let window = window;
1534 cx.defer(move |cx| {
1535 _ = window.update(cx, |_, window, cx| {
1536 window.dispatch_event(
1537 ScrollWheelEvent {
1538 position,
1539 delta: ScrollDelta::Pixels(point(px(0.), -delta)),
1540 ..Default::default()
1541 }
1542 .to_platform_input(),
1543 cx,
1544 );
1545 });
1546 });
1547 });
1548 }
1549
1550 fn update_participant_auto_scroll(&self, position: Point<Pixels>, cx: &mut App) {
1553 let Some((participant, registration)) = self.anchor_registration() else {
1554 return;
1555 };
1556 let visible_bounds = registration
1557 .bounds
1558 .intersect(®istration.hitbox.content_mask.bounds);
1559 let delta = if visible_bounds.size.width > px(0.) && visible_bounds.size.height > px(0.) {
1560 AutoScroll::compute_delta(position.y, visible_bounds)
1561 } else {
1562 None
1563 };
1564 participant.update(cx, |state, cx| state.set_auto_scroll(delta, cx));
1565 }
1566
1567 fn stop_anchor_auto_scroll(&mut self, cx: &mut App) {
1568 self.auto_scroll.stop();
1569 let Some(participant) = self.anchor_participant() else {
1570 return;
1571 };
1572 participant.update(cx, |state, cx| state.set_auto_scroll(None, cx));
1573 }
1574
1575 fn anchor_participant(&self) -> Option<Entity<SelectableTextState>> {
1577 self.anchor
1578 .as_ref()
1579 .filter(|anchor| anchor.inside)?
1580 .participant
1581 .as_ref()?
1582 .upgrade()
1583 }
1584
1585 fn anchor_registration(
1587 &self,
1588 ) -> Option<(Entity<SelectableTextState>, Rc<TextSelectionRegistration>)> {
1589 let participant = self.anchor_participant()?;
1590 let registration = self.participants.get(&participant.entity_id())?;
1591 Some((participant, registration.registration.clone()))
1592 }
1593
1594 fn prune_dead_participants(&mut self) {
1595 self.participants
1596 .retain(|_, registration| registration.participant.upgrade().is_some());
1597 }
1598}
1599
1600#[derive(Default)]
1601struct SelectionStateRegistry(HashMap<gpui::WindowId, WeakEntity<WindowSelectionState>>);
1604
1605impl Global for SelectionStateRegistry {}
1606
1607#[derive(Default)]
1608struct PendingTextSelectionScopes(HashMap<gpui::WindowId, TextSelectionScopeId>);
1609
1610impl Global for PendingTextSelectionScopes {}
1611
1612#[derive(Default)]
1613struct TextSelectionScopeStacks(HashMap<gpui::WindowId, Vec<TextSelectionScopeId>>);
1614
1615impl Global for TextSelectionScopeStacks {}
1616
1617fn push_text_selection_scope(window_id: gpui::WindowId, scope: TextSelectionScopeId, cx: &mut App) {
1618 if !cx.has_global::<TextSelectionScopeStacks>() {
1619 cx.set_global(TextSelectionScopeStacks::default());
1620 }
1621 cx.global_mut::<TextSelectionScopeStacks>()
1622 .0
1623 .entry(window_id)
1624 .or_default()
1625 .push(scope);
1626}
1627
1628fn pop_text_selection_scope(window_id: gpui::WindowId, cx: &mut App) {
1629 let stacks = &mut cx.global_mut::<TextSelectionScopeStacks>().0;
1630 let remove_stack = stacks.get_mut(&window_id).is_some_and(|stack| {
1631 stack.pop();
1632 stack.is_empty()
1633 });
1634 if remove_stack {
1635 stacks.remove(&window_id);
1636 }
1637}
1638
1639fn current_text_selection_scope(
1640 window_id: gpui::WindowId,
1641 cx: &App,
1642) -> Option<TextSelectionScopeId> {
1643 cx.has_global::<TextSelectionScopeStacks>()
1644 .then(|| {
1645 cx.global::<TextSelectionScopeStacks>()
1646 .0
1647 .get(&window_id)
1648 .and_then(|stack| stack.last().copied())
1649 })
1650 .flatten()
1651}
1652
1653fn with_text_selection_scope<T>(
1654 window_id: gpui::WindowId,
1655 scope: TextSelectionScopeId,
1656 cx: &mut App,
1657 callback: impl FnOnce(&mut App) -> T,
1658) -> T {
1659 push_text_selection_scope(window_id, scope, cx);
1660 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback(cx)));
1661 pop_text_selection_scope(window_id, cx);
1662 match result {
1663 Ok(result) => result,
1664 Err(payload) => std::panic::resume_unwind(payload),
1665 }
1666}
1667
1668pub struct TextSelection;
1670
1671impl TextSelection {
1672 pub fn selected_text(window: &mut Window, cx: &mut App) -> String {
1674 let Some(state) = live_text_selection_state(window, cx) else {
1675 return String::new();
1676 };
1677 let items = state.read(cx).copy_items(cx);
1678 resolve_copy_items(items, cx)
1679 }
1680
1681 pub fn has_selection(window: &mut Window, cx: &mut App) -> bool {
1684 live_text_selection_state(window, cx).is_some_and(|state| state.read(cx).has_selection(cx))
1685 }
1686
1687 pub fn clear(window: &mut Window, cx: &mut App) {
1689 if let Some(state) = live_text_selection_state(window, cx) {
1690 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
1691 dispatch_clear_handlers(handlers, cx);
1692 }
1693 }
1694
1695 pub fn clear_for_window(window_id: gpui::WindowId, cx: &mut App) {
1700 clear_window_text_selection(window_id, cx);
1701 }
1702
1703 pub fn end(window: &mut Window, cx: &mut App) {
1705 if let Some(state) = live_text_selection_state(window, cx) {
1706 state.update(cx, |state, cx| state.end(cx));
1707 }
1708 }
1709
1710 pub fn activate_scope(scope: TextSelectionScopeId, window: &mut Window, cx: &mut App) {
1712 let Some(state) = WindowSelectionState::existing(window, cx) else {
1713 if !cx.has_global::<PendingTextSelectionScopes>() {
1714 cx.set_global(PendingTextSelectionScopes::default());
1715 }
1716 cx.global_mut::<PendingTextSelectionScopes>()
1717 .0
1718 .insert(window.window_handle().window_id(), scope);
1719 return;
1720 };
1721 let handlers = state.update(cx, |state, cx| state.set_active_scope_state(scope, cx));
1722 dispatch_clear_handlers(handlers, cx);
1723 }
1724}
1725
1726pub struct TextSelectionLayer;
1731
1732pub(crate) fn text_selection_scope(
1733 scope: TextSelectionScopeId,
1734 element: impl IntoElement,
1735) -> impl IntoElement {
1736 TextSelectionScopeMarker {
1737 scope,
1738 element: element.into_element(),
1739 }
1740}
1741
1742struct TextSelectionScopeMarker<E> {
1743 scope: TextSelectionScopeId,
1744 element: E,
1745}
1746
1747impl<E: Element> IntoElement for TextSelectionScopeMarker<E> {
1748 type Element = Self;
1749
1750 fn into_element(self) -> Self::Element {
1751 self
1752 }
1753}
1754
1755impl<E: Element> Element for TextSelectionScopeMarker<E> {
1756 type RequestLayoutState = E::RequestLayoutState;
1757 type PrepaintState = E::PrepaintState;
1758
1759 fn id(&self) -> Option<ElementId> {
1760 self.element.id()
1761 }
1762
1763 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1764 self.element.source_location()
1765 }
1766
1767 fn request_layout(
1768 &mut self,
1769 id: Option<&GlobalElementId>,
1770 inspector_id: Option<&InspectorElementId>,
1771 window: &mut Window,
1772 cx: &mut App,
1773 ) -> (LayoutId, Self::RequestLayoutState) {
1774 let window_id = window.window_handle().window_id();
1775 with_text_selection_scope(window_id, self.scope, cx, |cx| {
1776 self.element.request_layout(id, inspector_id, window, cx)
1777 })
1778 }
1779
1780 fn prepaint(
1781 &mut self,
1782 id: Option<&GlobalElementId>,
1783 inspector_id: Option<&InspectorElementId>,
1784 bounds: Bounds<Pixels>,
1785 request_layout: &mut Self::RequestLayoutState,
1786 window: &mut Window,
1787 cx: &mut App,
1788 ) -> Self::PrepaintState {
1789 let window_id = window.window_handle().window_id();
1790 with_text_selection_scope(window_id, self.scope, cx, |cx| {
1791 self.element
1792 .prepaint(id, inspector_id, bounds, request_layout, window, cx)
1793 })
1794 }
1795
1796 fn paint(
1797 &mut self,
1798 id: Option<&GlobalElementId>,
1799 inspector_id: Option<&InspectorElementId>,
1800 bounds: Bounds<Pixels>,
1801 request_layout: &mut Self::RequestLayoutState,
1802 prepaint: &mut Self::PrepaintState,
1803 window: &mut Window,
1804 cx: &mut App,
1805 ) {
1806 let window_id = window.window_handle().window_id();
1807 with_text_selection_scope(window_id, self.scope, cx, |cx| {
1808 self.element.paint(
1809 id,
1810 inspector_id,
1811 bounds,
1812 request_layout,
1813 prepaint,
1814 window,
1815 cx,
1816 );
1817 });
1818 }
1819}
1820
1821#[doc(hidden)]
1822pub struct TextSelectionLayerPrepaintState(Entity<WindowSelectionState>);
1823
1824impl IntoElement for TextSelectionLayer {
1825 type Element = Self;
1826
1827 fn into_element(self) -> Self::Element {
1828 self
1829 }
1830}
1831
1832impl Element for TextSelectionLayer {
1833 type RequestLayoutState = ();
1834 type PrepaintState = TextSelectionLayerPrepaintState;
1835
1836 fn id(&self) -> Option<ElementId> {
1837 Some("window-text-selection".into())
1838 }
1839
1840 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1841 None
1842 }
1843
1844 fn request_layout(
1845 &mut self,
1846 _: Option<&GlobalElementId>,
1847 _: Option<&InspectorElementId>,
1848 window: &mut Window,
1849 cx: &mut App,
1850 ) -> (LayoutId, Self::RequestLayoutState) {
1851 (window.request_layout(Style::default(), [], cx), ())
1852 }
1853
1854 fn prepaint(
1855 &mut self,
1856 global_id: Option<&GlobalElementId>,
1857 _: Option<&InspectorElementId>,
1858 _: Bounds<Pixels>,
1859 _: &mut Self::RequestLayoutState,
1860 window: &mut Window,
1861 cx: &mut App,
1862 ) -> Self::PrepaintState {
1863 GlobalState::init(cx);
1869 GlobalState::global_mut(cx).begin_selection_frame();
1870 TextSelectionLayerPrepaintState(retain_text_selection_state(global_id, window, cx))
1871 }
1872
1873 fn paint(
1874 &mut self,
1875 _: Option<&GlobalElementId>,
1876 _: Option<&InspectorElementId>,
1877 _: Bounds<Pixels>,
1878 _: &mut Self::RequestLayoutState,
1879 state: &mut Self::PrepaintState,
1880 window: &mut Window,
1881 cx: &mut App,
1882 ) {
1883 paint_text_selection(&state.0, window, cx);
1884 }
1885}
1886
1887fn retain_text_selection_state(
1888 global_id: Option<&GlobalElementId>,
1889 window: &mut Window,
1890 cx: &mut App,
1891) -> Entity<WindowSelectionState> {
1892 let window_id = window.window_handle().window_id();
1893 let state = window.with_element_state::<Entity<WindowSelectionState>, _>(
1894 global_id.expect("TextSelection has a stable element id"),
1895 |retained, _| {
1896 let state = retained.unwrap_or_else(|| WindowSelectionState::acquire(window_id, cx));
1897 (state.clone(), state)
1898 },
1899 );
1900 if !cx.has_global::<SelectionStateRegistry>() {
1901 cx.set_global(SelectionStateRegistry::default());
1902 }
1903 cx.global_mut::<SelectionStateRegistry>()
1904 .0
1905 .insert(window_id, state.downgrade());
1906 state
1907}
1908
1909fn paint_text_selection(state: &Entity<WindowSelectionState>, window: &mut Window, cx: &mut App) {
1910 if state.update(cx, |state, _| state.schedule_finish_frame()) {
1911 let state = state.downgrade();
1912 window.defer(cx, move |window, cx| {
1913 let Some(state) = state.upgrade() else {
1914 return;
1915 };
1916 let handlers = state.update(cx, |state, cx| state.finish_frame(cx));
1917 dispatch_clear_handlers(handlers, cx);
1918 let refresh_cursor = state.update(cx, |state, cx| {
1921 if std::mem::take(&mut state.refresh_held_cursor) && state.is_selecting {
1922 state.update_in_window(window.mouse_position(), window, cx);
1923 true
1924 } else {
1925 false
1926 }
1927 });
1928 if refresh_cursor {
1929 WindowSelectionState::resolve_content_keys(&state, cx);
1930 }
1931 });
1932 }
1933
1934 let mouse_down_state = state.downgrade();
1935 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
1936 if event.button != MouseButton::Left {
1937 return;
1938 }
1939 let Some(state) = mouse_down_state.upgrade() else {
1940 return;
1941 };
1942 if phase.capture() {
1943 GlobalState::init(cx);
1944 GlobalState::reset_text_selection_suppression(cx);
1945 let handlers = state.update(cx, |state, cx| {
1946 if state.mouse_down_prepared {
1947 return Vec::new();
1948 }
1949 state.mouse_down_prepared = true;
1950 state.prepare_for_mouse_down(event.click_count == 1 && event.modifiers.shift, cx)
1951 });
1952 dispatch_clear_handlers(handlers, cx);
1953 } else if event.click_count == 1 {
1954 if GlobalState::is_text_selection_suppressed(cx) {
1955 state.update(cx, |state, _| state.pending_extension_anchor = None);
1956 return;
1957 }
1958 state.update(cx, |state, cx| {
1959 if !state.is_selecting {
1960 state.begin_in_window(event.position, event.modifiers.shift, window, cx)
1961 }
1962 });
1963 WindowSelectionState::resolve_content_keys(&state, cx);
1964 } else if event.click_count >= 2 {
1965 if GlobalState::is_text_selection_suppressed(cx) {
1966 return;
1967 }
1968 state.update(cx, |state, cx| {
1969 state.select_at(event.position, event.click_count, window, cx)
1970 });
1971 WindowSelectionState::resolve_content_keys(&state, cx);
1972 }
1973 });
1974
1975 let mouse_move_state = state.downgrade();
1976 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
1977 if phase.bubble()
1978 && let Some(state) = mouse_move_state.upgrade()
1979 {
1980 state.update(cx, |state, cx| {
1981 state.update_in_window(event.position, window, cx)
1982 });
1983 WindowSelectionState::resolve_content_keys(&state, cx);
1984 }
1985 });
1986
1987 let mouse_up_state = state.downgrade();
1988 window.on_mouse_event(move |_: &MouseUpEvent, phase, _, cx| {
1989 if phase.bubble()
1990 && let Some(state) = mouse_up_state.upgrade()
1991 {
1992 state.update(cx, |state, cx| {
1993 state.mouse_down_prepared = false;
1994 state.end(cx)
1995 });
1996 }
1997 });
1998
1999 let scroll_state = state.downgrade();
2000 window.on_mouse_event(move |_: &ScrollWheelEvent, phase, window, cx| {
2001 if phase.bubble()
2002 && let Some(state) = scroll_state.upgrade()
2003 {
2004 let position = window.mouse_position();
2005 state.update(cx, |state, cx| state.update_in_window(position, window, cx));
2006 WindowSelectionState::resolve_content_keys(&state, cx);
2007 }
2008 });
2009}
2010
2011fn live_text_selection_state(
2012 window: &Window,
2013 cx: &mut App,
2014) -> Option<Entity<WindowSelectionState>> {
2015 WindowSelectionState::existing(window, cx)
2016}
2017
2018pub(crate) fn clear_window_text_selection(window_id: gpui::WindowId, cx: &mut App) {
2019 if !cx.has_global::<SelectionStateRegistry>() {
2020 return;
2021 }
2022 let Some(state) = cx
2023 .global::<SelectionStateRegistry>()
2024 .0
2025 .get(&window_id)
2026 .and_then(WeakEntity::upgrade)
2027 else {
2028 return;
2029 };
2030 let handlers = state.update(cx, |state, cx| state.clear_state(cx));
2031 dispatch_clear_handlers(handlers, cx);
2032}
2033
2034#[cfg(test)]
2035mod tests {
2036 use super::*;
2037 use crate::ElementExt as _;
2038 use gpui::{
2039 Bounds, ContentMask, Context, Hitbox, HitboxBehavior, HitboxId, InteractiveElement as _,
2040 IntoElement, ParentElement as _, Render, SharedString, Styled as _, StyledText,
2041 TestAppContext, TextLayout, Window, div, point, prelude::FluentBuilder as _, px, size,
2042 };
2043 use std::{
2044 cell::{Cell, RefCell},
2045 rc::Rc,
2046 };
2047
2048 struct FakeParticipant {
2049 selection: TextSelectionHandle,
2050 }
2051
2052 struct WindowSelectionView {
2053 selection: TextSelectionHandle,
2054 }
2055
2056 struct SelectionElementOnlyView;
2057 struct ToggleSelectionElementView {
2058 enabled: bool,
2059 selection: TextSelectionHandle,
2060 }
2061
2062 struct DoubleSelectionElementView {
2063 selection: TextSelectionHandle,
2064 }
2065
2066 struct WindowOwnedSelectionView {
2067 selection: TextSelectionHandle,
2068 }
2069
2070 struct FirstFrameScopedSelectionView {
2071 selection: TextSelectionHandle,
2072 }
2073
2074 struct PlainRunLayoutView {
2075 texts: Vec<SharedString>,
2076 layouts: Vec<TextLayout>,
2077 }
2078
2079 impl Render for WindowSelectionView {
2080 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2081 div()
2082 }
2083 }
2084
2085 impl Render for SelectionElementOnlyView {
2086 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2087 div()
2088 .size_full()
2089 .child(TextSelectionLayer)
2090 .child(
2091 div()
2092 .size_full()
2093 .on_mouse_down(MouseButton::Left, |_, _, cx| {
2094 GlobalState::suppress_text_selection(cx);
2095 }),
2096 )
2097 }
2098 }
2099
2100 impl Render for ToggleSelectionElementView {
2101 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2102 let selection = self.selection.clone();
2103 div().when(self.enabled, |this| {
2104 this.child(TextSelectionLayer)
2105 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
2106 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2107 selection.register(
2108 TextSelectionRegistration::new(hitbox, bounds)
2109 .with_text_bounds(vec![bounds]),
2110 window,
2111 cx,
2112 );
2113 }))
2114 })
2115 }
2116 }
2117
2118 impl Render for DoubleSelectionElementView {
2119 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2120 let selection = self.selection.clone();
2121 div()
2122 .size_full()
2123 .child(TextSelectionLayer)
2124 .child(TextSelectionLayer)
2125 .on_prepaint(move |bounds, window, cx| {
2126 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2127 selection.register(
2128 TextSelectionRegistration::new(hitbox, bounds)
2129 .with_text_bounds(vec![bounds]),
2130 window,
2131 cx,
2132 );
2133 })
2134 }
2135 }
2136
2137 impl Render for WindowOwnedSelectionView {
2138 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2139 let selection = self.selection.clone();
2140 div()
2141 .size_full()
2142 .child(TextSelectionLayer)
2143 .child(div().size_full().on_prepaint(move |bounds, window, cx| {
2144 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2145 selection.register(
2146 TextSelectionRegistration::new(hitbox, bounds)
2147 .with_text_bounds(vec![bounds]),
2148 window,
2149 cx,
2150 );
2151 }))
2152 }
2153 }
2154
2155 impl Render for FirstFrameScopedSelectionView {
2156 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2157 let scope = TextSelectionScopeId::from_raw(23);
2158 TextSelection::activate_scope(scope, window, cx);
2159 let selection = self.selection.clone();
2160
2161 div().child(TextSelectionLayer).child(
2162 div()
2163 .size_full()
2164 .on_prepaint(move |bounds, window, cx| {
2165 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
2166 selection.register(
2167 TextSelectionRegistration::new(hitbox, bounds)
2168 .with_text_bounds(vec![bounds]),
2169 window,
2170 cx,
2171 );
2172 })
2173 .text_selection_scope(scope),
2174 )
2175 }
2176 }
2177
2178 impl Render for PlainRunLayoutView {
2179 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2180 self.layouts.clear();
2181 let children = self
2182 .texts
2183 .iter()
2184 .enumerate()
2185 .map(|(index, text)| {
2186 let text = StyledText::new(text.clone());
2187 self.layouts.push(text.layout().clone());
2188 div().absolute().top(px(index as f32 * 40.)).child(text)
2189 })
2190 .collect::<Vec<_>>();
2191 div().size_full().children(children)
2192 }
2193 }
2194
2195 impl FakeParticipant {
2196 fn new(text: &str, cx: &mut gpui::App) -> Self {
2197 let selection = TextSelectionHandle::new(text, cx);
2198 Self { selection }
2199 }
2200
2201 fn register(
2202 &self,
2203 selection_state: &mut WindowSelectionState,
2204 y: f32,
2205 scope: TextSelectionScopeId,
2206 document_order: u64,
2207 cx: &mut gpui::App,
2208 ) {
2209 let bounds = Bounds::new(point(px(0.), px(y)), size(px(100.), px(10.)));
2210 selection_state.register_participant(
2211 self.selection.clone(),
2212 TextSelectionRegistration::new(
2213 Hitbox {
2214 id: HitboxId::placeholder(),
2215 bounds,
2216 content_mask: ContentMask { bounds },
2217 behavior: HitboxBehavior::Normal,
2218 },
2219 bounds,
2220 )
2221 .with_scope(scope)
2222 .with_document_order(document_order)
2223 .with_text_bounds(vec![bounds]),
2224 cx,
2225 );
2226 }
2227 }
2228
2229 fn laid_out_runs(texts: &[&str], cx: &mut TestAppContext) -> Vec<(SharedString, TextLayout)> {
2230 let texts = texts
2231 .iter()
2232 .map(|text| SharedString::from(*text))
2233 .collect::<Vec<_>>();
2234 let view = cx.add_window({
2235 let texts = texts.clone();
2236 move |_, _| PlainRunLayoutView {
2237 texts,
2238 layouts: Vec::new(),
2239 }
2240 });
2241 cx.update_window(*view, |_, window, cx| {
2242 let _ = window.draw(cx);
2243 })
2244 .unwrap();
2245 let layouts = cx.update(|cx| view.read(cx).unwrap().layouts.clone());
2246 texts.into_iter().zip(layouts).collect()
2247 }
2248
2249 fn plain_snapshot(anchor: Point<Pixels>, cursor: Point<Pixels>) -> TextSelectionSnapshot {
2250 TextSelectionSnapshot::new(
2251 TextSelectionEndpoint::new(None, anchor),
2252 TextSelectionEndpoint::new(None, cursor),
2253 )
2254 .with_window_points(Some(TextSelectionWindowPoints { anchor, cursor }))
2255 }
2256
2257 #[gpui::test]
2258 fn scope_stack_is_cleaned_after_panicking_subtree(cx: &mut TestAppContext) {
2259 let window_id = {
2260 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2261 window_cx.update(|window, _| window.window_handle().window_id())
2262 };
2263 let scope = TextSelectionScopeId::from_raw(41);
2264
2265 cx.update(|cx| {
2266 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2267 with_text_selection_scope(window_id, scope, cx, |_| panic!("subtree failed"));
2268 }));
2269
2270 assert!(result.is_err());
2271 assert_eq!(current_text_selection_scope(window_id, cx), None);
2272 });
2273 }
2274
2275 #[gpui::test]
2276 fn reentrant_scope_from_one_window_does_not_pollute_another(cx: &mut TestAppContext) {
2277 let first_window_id = {
2278 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2279 window_cx.update(|window, _| window.window_handle().window_id())
2280 };
2281 let second_window_id = {
2282 let (_, window_cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2283 window_cx.update(|window, _| window.window_handle().window_id())
2284 };
2285 let scope = TextSelectionScopeId::from_raw(42);
2286
2287 cx.update(|cx| {
2288 with_text_selection_scope(first_window_id, scope, cx, |cx| {
2289 assert_eq!(current_text_selection_scope(second_window_id, cx), None);
2290 assert_eq!(
2291 current_text_selection_scope(first_window_id, cx),
2292 Some(scope)
2293 );
2294 });
2295 });
2296 }
2297
2298 #[gpui::test]
2299 fn selection_callback_can_reenter_its_selection_state(cx: &mut TestAppContext) {
2300 let called = Rc::new(Cell::new(false));
2301 let called_from_callback = called.clone();
2302 let (selection_state, participant) = cx.update(|cx| {
2303 let selection_state = cx.new(|_| WindowSelectionState::default());
2304 let selection_state_for_callback = selection_state.clone();
2305 let participant = FakeParticipant::new("participant", cx);
2306 participant
2307 .selection
2308 .subscribe(
2309 move |event, cx| {
2310 if matches!(event, TextSelectionEvent::SelectionChanged(Some(_))) {
2311 selection_state_for_callback
2312 .update(cx, |_, _| called_from_callback.set(true));
2313 }
2314 },
2315 cx,
2316 )
2317 .detach();
2318 (selection_state, participant)
2319 });
2320 cx.run_until_parked();
2321 cx.update(|cx| {
2322 selection_state.update(cx, |selection_state, cx| {
2323 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx);
2324 selection_state.begin(point(px(1.), px(1.)), false, cx);
2325 selection_state.update(point(px(20.), px(1.)), cx);
2326 });
2327 });
2328 cx.run_until_parked();
2329 assert!(called.get());
2330 }
2331
2332 #[gpui::test]
2333 fn selection_events_preserve_snapshot_then_clear_order(cx: &mut TestAppContext) {
2334 let observed = Rc::new(RefCell::new(Vec::new()));
2335 let observed_for_callback = observed.clone();
2336 let selection = cx.update(|cx| {
2337 let selection = TextSelectionHandle::new("selection", cx);
2338 selection
2339 .subscribe(
2340 move |event, _| {
2341 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
2342 observed_for_callback.borrow_mut().push(snapshot.is_some());
2343 }
2344 },
2345 cx,
2346 )
2347 .detach();
2348 selection
2349 });
2350 cx.run_until_parked();
2351 cx.update(|cx| {
2352 selection.0.update(cx, |state, cx| {
2353 state.set_snapshot(
2354 Some(plain_snapshot(point(px(1.), px(1.)), point(px(8.), px(1.)))),
2355 cx,
2356 );
2357 state.clear_state(cx);
2358 });
2359 });
2360 cx.run_until_parked();
2361 assert_eq!(&*observed.borrow(), &[true, false]);
2362 }
2363
2364 fn text_run(order: u64, text: SharedString, layout: TextLayout) -> TextSelectionRun {
2365 let bounds = layout.bounds();
2366 TextSelectionRun::new(text, layout, bounds).with_document_order(order)
2367 }
2368
2369 #[gpui::test]
2370 fn public_selection_data_uses_builders_and_readers(cx: &mut TestAppContext) {
2371 let bounds = Bounds::new(point(px(1.), px(2.)), size(px(30.), px(10.)));
2372 let hitbox = Hitbox {
2373 id: HitboxId::placeholder(),
2374 bounds,
2375 content_mask: ContentMask { bounds },
2376 behavior: HitboxBehavior::Normal,
2377 };
2378 let scope = TextSelectionScopeId::from_raw(7);
2379 let endpoint = TextSelectionEndpoint::new(None, bounds.origin)
2380 .with_content_key(TextSelectionContentKey::new(11));
2381 let snapshot = TextSelectionSnapshot::new(endpoint, endpoint)
2382 .with_selecting(true)
2383 .with_window_points(Some(TextSelectionWindowPoints {
2384 anchor: bounds.origin,
2385 cursor: bounds.bottom_right(),
2386 }))
2387 .with_coverage(TextSelectionCoverage::Full);
2388 let registration = TextSelectionRegistration::new(hitbox, bounds)
2389 .with_scroll_offset(point(px(3.), px(4.)))
2390 .with_scope(scope)
2391 .with_document_order(9)
2392 .with_text_bounds(vec![bounds]);
2393
2394 assert_eq!(endpoint.entity_id(), None);
2395 assert_eq!(endpoint.content_point(), bounds.origin);
2396 assert_eq!(
2397 endpoint.content_key(),
2398 Some(TextSelectionContentKey::new(11))
2399 );
2400 assert_eq!(snapshot.anchor(), endpoint);
2401 assert_eq!(snapshot.cursor(), endpoint);
2402 assert!(snapshot.is_selecting());
2403 assert_eq!(snapshot.coverage(), TextSelectionCoverage::Full);
2404 assert_eq!(
2405 snapshot.window_points(),
2406 Some(TextSelectionWindowPoints {
2407 anchor: bounds.origin,
2408 cursor: bounds.bottom_right(),
2409 })
2410 );
2411 assert_eq!(registration.bounds(), bounds);
2412 assert_eq!(registration.scroll_offset(), point(px(3.), px(4.)));
2413 assert_eq!(registration.scope(), scope);
2414 assert_eq!(registration.document_order(), 9);
2415 assert_eq!(registration.text_bounds(), &[bounds]);
2416
2417 let (text, layout) = laid_out_runs(&["aé"], cx).pop().unwrap();
2418 let text_run = TextSelectionRun::new(text.clone(), layout.clone(), layout.bounds())
2419 .with_document_order(3);
2420 assert_eq!(text_run.document_order(), 3);
2421 assert_eq!(text_run.text(), &text);
2422 assert_eq!(text_run.layout().len(), layout.len());
2423 assert_eq!(text_run.bounds(), layout.bounds());
2424
2425 let projection = TextSelectionProjection {
2426 ranges: vec![Some(1..3)],
2427 is_active: true,
2428 };
2429 assert_eq!(projection.ranges(), &[Some(1..3)]);
2430 assert!(projection.is_active());
2431 }
2432
2433 #[gpui::test]
2434 fn selection_handle_is_the_public_adapter_seam(cx: &mut TestAppContext) {
2435 let selected = Rc::new(Cell::new(false));
2436 let selected_from_callback = selected.clone();
2437 cx.update(|cx| {
2438 let selection = TextSelectionHandle::new("initial", cx);
2439 let entity_id = selection.entity_id();
2440 selection.set_fallback_copy_text("updated", cx);
2441 selection.set_local_selection(true, cx);
2442 selection
2443 .subscribe(
2444 move |event, _| {
2445 if let TextSelectionEvent::SelectionChanged(snapshot) = event {
2446 selected_from_callback.set(snapshot.is_some());
2447 }
2448 },
2449 cx,
2450 )
2451 .detach();
2452 selection.focus_with(|_, _| {}, cx);
2453 selection.copy_with(|_| "copied".to_string(), cx);
2454 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(3)), cx);
2455
2456 assert_eq!(selection.entity_id(), entity_id);
2457 assert_eq!(selection.snapshot(cx), None);
2458 assert_eq!(
2459 selection.update_runs(&[], cx),
2460 TextSelectionProjection::default()
2461 );
2462 });
2463 assert!(!selected.get());
2464 }
2465
2466 #[gpui::test]
2467 fn selection_handle_can_subscribe_its_window_to_refresh(cx: &mut TestAppContext) {
2468 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
2469 selection: TextSelectionHandle::new("refresh", cx),
2470 });
2471 cx.update(|window, cx| {
2472 let selection = TextSelectionHandle::new("refresh", cx);
2473 selection.refresh_window_on_change(window, cx).detach();
2474 });
2475 }
2476
2477 #[gpui::test]
2478 fn plain_projection_preserves_forward_reversed_and_unicode_ranges(cx: &mut TestAppContext) {
2479 let (text, layout) = laid_out_runs(&["aé🙂z"], cx).pop().unwrap();
2480 let run = text_run(0, text, layout.clone());
2481 let start = layout.position_for_index(1).unwrap();
2482 let end = layout.position_for_index(7).unwrap();
2483
2484 let forward = project_ranges(Some(plain_snapshot(start, end)), std::slice::from_ref(&run));
2485 let reversed = project_ranges(Some(plain_snapshot(end, start)), &[run]);
2486
2487 assert_eq!(forward.ranges(), &[Some(1..7)]);
2488 assert_eq!(reversed.ranges(), &[Some(1..7)]);
2489 assert!(forward.is_active());
2490 assert!(reversed.is_active());
2491 }
2492
2493 #[gpui::test]
2494 fn double_click_expands_a_plain_run_to_the_input_word_boundary(cx: &mut TestAppContext) {
2495 let (text, layout) = laid_out_runs(&["one café, three"], cx).pop().unwrap();
2496 let run = text_run(0, text, layout.clone());
2497 let click = layout.position_for_index(6).unwrap();
2498
2499 let (anchor, cursor) =
2500 points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
2501 let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
2502
2503 assert_eq!(states.ranges(), &[Some(4..9)]);
2504 }
2505
2506 #[gpui::test]
2507 fn multi_click_uses_text_layout_window_coordinates_at_a_nonzero_origin(
2508 cx: &mut TestAppContext,
2509 ) {
2510 let mut runs = laid_out_runs(&["above", "alpha beta"], cx);
2511 let (text, layout) = runs.pop().unwrap();
2512 assert!(layout.bounds().origin.y > px(0.));
2513 let run = text_run(0, text, layout.clone());
2514 let click = layout.position_for_index(7).unwrap();
2515
2516 let (anchor, cursor) =
2517 points_for_multi_click(std::slice::from_ref(&run), click, 2).unwrap();
2518 let projection = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
2519
2520 assert_eq!(projection.ranges(), &[Some(6..10)]);
2521 }
2522
2523 #[gpui::test]
2524 fn triple_click_expands_to_the_input_logical_line_not_the_visual_row(cx: &mut TestAppContext) {
2525 let (text, layout) = laid_out_runs(&["second line"], cx).pop().unwrap();
2526 let run = text_run(0, text, layout.clone());
2527 let click = layout.position_for_index(4).unwrap();
2528
2529 let (anchor, cursor) =
2530 points_for_multi_click(std::slice::from_ref(&run), click, 4).unwrap();
2531 let states = project_ranges(Some(plain_snapshot(anchor, cursor)), &[run]);
2532
2533 assert_eq!(states.ranges(), &[Some(0..11)]);
2534 assert_eq!(line_range_at("first line\nsecond line\nthird", 15), 11..22);
2535 }
2536
2537 #[gpui::test]
2538 fn plain_projection_spans_multiple_runs_and_leaves_empty_gutters_unselected(
2539 cx: &mut TestAppContext,
2540 ) {
2541 let mut runs = laid_out_runs(&["first", "", "second"], cx);
2542 let (first_text, first_layout) = runs.remove(0);
2543 let (gutter_text, gutter_layout) = runs.remove(0);
2544 let (second_text, second_layout) = runs.remove(0);
2545 let start = first_layout.position_for_index(2).unwrap();
2546 let end = second_layout.position_for_index(3).unwrap();
2547 let states = project_ranges(
2548 Some(plain_snapshot(start, end)),
2549 &[
2550 text_run(2, second_text, second_layout),
2551 text_run(1, gutter_text, gutter_layout),
2552 text_run(0, first_text, first_layout),
2553 ],
2554 );
2555
2556 assert_eq!(states.ranges(), &[Some(0..3), None, Some(2..5)]);
2557 assert!(states.is_active());
2558 }
2559
2560 #[gpui::test]
2561 fn plain_projection_caches_multiple_participant_copies_in_document_order(
2562 cx: &mut TestAppContext,
2563 ) {
2564 let mut runs = laid_out_runs(&["one", "two"], cx);
2565 let (first_text, first_layout) = runs.remove(0);
2566 let (second_text, second_layout) = runs.remove(0);
2567 let snapshot = plain_snapshot(
2568 first_layout.position_for_index(1).unwrap(),
2569 second_layout.position_for_index(2).unwrap(),
2570 );
2571 cx.update(|cx| {
2572 let mut selection_state = WindowSelectionState::default();
2573 let first = FakeParticipant::new("", cx);
2574 let second = FakeParticipant::new("", cx);
2575 first.register(
2576 &mut selection_state,
2577 0.,
2578 TextSelectionScopeId::default(),
2579 1,
2580 cx,
2581 );
2582 second.register(
2583 &mut selection_state,
2584 20.,
2585 TextSelectionScopeId::default(),
2586 0,
2587 cx,
2588 );
2589
2590 first
2591 .selection
2592 .0
2593 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
2594 let projection = first
2595 .selection
2596 .update_runs(&[text_run(0, first_text, first_layout)], cx);
2597 assert_eq!(projection.ranges(), &[Some(1..3)]);
2598 assert!(projection.is_active());
2599 second
2600 .selection
2601 .0
2602 .update(cx, |state, cx| state.set_snapshot(Some(snapshot), cx));
2603 let projection = second
2604 .selection
2605 .update_runs(&[text_run(0, second_text, second_layout)], cx);
2606 assert_eq!(projection.ranges(), &[Some(0..2)]);
2607 assert!(projection.is_active());
2608
2609 assert_eq!(selection_state.selected_text(cx), "tw\nne");
2610 });
2611 }
2612
2613 #[gpui::test]
2614 fn plain_projection_invalidates_cached_copy_when_the_snapshot_changes(cx: &mut TestAppContext) {
2615 let (text, layout) = laid_out_runs(&["first"], cx).pop().unwrap();
2616 let first_snapshot = plain_snapshot(
2617 layout.position_for_index(1).unwrap(),
2618 layout.position_for_index(3).unwrap(),
2619 );
2620 let changed_snapshot = plain_snapshot(
2621 layout.position_for_index(3).unwrap(),
2622 layout.position_for_index(5).unwrap(),
2623 );
2624 let run = text_run(0, text, layout);
2625 cx.update(|cx| {
2626 let mut selection_state = WindowSelectionState::default();
2627 let participant = FakeParticipant::new("", cx);
2628 participant.register(
2629 &mut selection_state,
2630 0.,
2631 TextSelectionScopeId::default(),
2632 0,
2633 cx,
2634 );
2635 participant.selection.0.update(cx, |state, cx| {
2636 state.set_snapshot(Some(first_snapshot), cx);
2637 state.update_runs(std::slice::from_ref(&run));
2638 });
2639 assert_eq!(selection_state.selected_text(cx), "ir");
2640
2641 participant.selection.0.update(cx, |state, cx| {
2642 state.set_snapshot(Some(changed_snapshot), cx);
2643 });
2644 assert_eq!(selection_state.selected_text(cx), "");
2645
2646 participant.selection.update_runs(&[run], cx);
2647 assert_eq!(selection_state.selected_text(cx), "st");
2648 selection_state.clear(cx);
2649 participant.selection.set_local_selection(true, cx);
2650 assert_eq!(selection_state.selected_text(cx), "");
2651 });
2652 }
2653
2654 #[gpui::test]
2655 fn plain_projection_orders_cached_runs_by_frame_order_not_input_order(cx: &mut TestAppContext) {
2656 let mut runs = laid_out_runs(&["one", "two"], cx);
2657 let (first_text, first_layout) = runs.remove(0);
2658 let (second_text, second_layout) = runs.remove(0);
2659 let snapshot = plain_snapshot(
2660 first_layout.position_for_index(1).unwrap(),
2661 second_layout.position_for_index(2).unwrap(),
2662 );
2663 cx.update(|cx| {
2664 let mut selection_state = WindowSelectionState::default();
2665 let participant = FakeParticipant::new("", cx);
2666 participant.register(
2667 &mut selection_state,
2668 0.,
2669 TextSelectionScopeId::default(),
2670 0,
2671 cx,
2672 );
2673 participant.selection.0.update(cx, |state, cx| {
2674 state.set_snapshot(Some(snapshot), cx);
2675 state.update_runs(&[
2676 text_run(1, first_text, first_layout),
2677 text_run(0, second_text, second_layout),
2678 ]);
2679 });
2680
2681 assert_eq!(selection_state.selected_text(cx), "twne");
2682 });
2683 }
2684
2685 #[gpui::test]
2686 fn plain_projection_safely_rejects_a_text_layout_length_mismatch(cx: &mut TestAppContext) {
2687 let (_, layout) = laid_out_runs(&["short"], cx).pop().unwrap();
2688 let start = layout.position_for_index(0).unwrap();
2689 let end = layout.position_for_index(5).unwrap();
2690 let states = project_ranges(
2691 Some(plain_snapshot(start, end)),
2692 &[text_run(0, SharedString::from("longer"), layout)],
2693 );
2694
2695 assert_eq!(states.ranges(), &[None]);
2696 assert!(states.is_active());
2697 }
2698
2699 #[gpui::test]
2700 fn begin_update_and_end_publish_a_cross_participant_selection(cx: &mut TestAppContext) {
2701 cx.update(|cx| {
2702 let mut selection_state = WindowSelectionState::default();
2703 let first = FakeParticipant::new("first", cx);
2704 let second = FakeParticipant::new("second", cx);
2705 first.register(
2706 &mut selection_state,
2707 0.,
2708 TextSelectionScopeId::default(),
2709 0,
2710 cx,
2711 );
2712 second.register(
2713 &mut selection_state,
2714 20.,
2715 TextSelectionScopeId::default(),
2716 1,
2717 cx,
2718 );
2719
2720 selection_state.begin(point(px(1.), px(1.)), false, cx);
2721 selection_state.update(point(px(1.), px(25.)), cx);
2722 assert!(selection_state.has_selection(cx));
2723 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
2724
2725 selection_state.end(cx);
2726 assert!(!selection_state.is_selecting());
2727 });
2728 }
2729
2730 #[gpui::test]
2731 fn shift_extension_keeps_its_original_anchor_when_reversed(cx: &mut TestAppContext) {
2732 cx.update(|cx| {
2733 let mut selection_state = WindowSelectionState::default();
2734 let participant = FakeParticipant::new("participant", cx);
2735 participant.register(
2736 &mut selection_state,
2737 0.,
2738 TextSelectionScopeId::default(),
2739 0,
2740 cx,
2741 );
2742
2743 selection_state.begin(point(px(2.), px(2.)), false, cx);
2744 selection_state.end(cx);
2745 selection_state.begin(point(px(8.), px(2.)), true, cx);
2746 selection_state.end(cx);
2747 let first_anchor = selection_state.snapshot().unwrap().anchor();
2748
2749 selection_state.begin(point(px(0.), px(2.)), true, cx);
2750 selection_state.end(cx);
2751 let reversed = selection_state.snapshot().unwrap();
2752 assert_eq!(reversed.anchor(), first_anchor);
2753 assert!(reversed.cursor().content_point().x < reversed.anchor().content_point().x);
2754 });
2755 }
2756
2757 #[gpui::test]
2758 fn content_key_resolver_runs_outside_the_window_state_lease(cx: &mut TestAppContext) {
2759 cx.update(|cx| {
2760 let state = cx.new(|_| WindowSelectionState::default());
2761 let participant = FakeParticipant::new("virtual", cx);
2762 let state_for_callback = state.clone();
2763 participant.selection.resolve_content_key_with(
2764 move |_, cx| {
2765 let _ = state_for_callback.read(cx).snapshot();
2766 Some(TextSelectionContentKey::new(7))
2767 },
2768 cx,
2769 );
2770 state.update(cx, |state, cx| {
2771 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
2772 state.begin(point(px(1.), px(1.)), false, cx);
2773 state.update(point(px(8.), px(1.)), cx);
2774 });
2775
2776 WindowSelectionState::resolve_content_keys(&state, cx);
2777
2778 assert_eq!(
2779 state.read(cx).snapshot().unwrap().cursor().content_key(),
2780 Some(TextSelectionContentKey::new(7))
2781 );
2782 });
2783 }
2784
2785 #[gpui::test]
2786 fn active_dnd_does_not_move_a_text_selection_cursor(cx: &mut TestAppContext) {
2787 let window = cx.add_window(|_, cx| WindowSelectionView {
2788 selection: TextSelectionHandle::new("unused", cx),
2789 });
2790 window
2791 .update(cx, |_, window, cx| {
2792 let mut state = WindowSelectionState::default();
2793 let participant = FakeParticipant::new("participant", cx);
2794 participant.register(&mut state, 0., TextSelectionScopeId::default(), 0, cx);
2795 state.begin(point(px(1.), px(1.)), false, cx);
2796 let before = state.cursor.as_ref().unwrap().point;
2797 state.update_in_window_with_active_drag(point(px(80.), px(1.)), true, window, cx);
2798 assert_eq!(state.cursor.as_ref().unwrap().point, before);
2799 })
2800 .unwrap();
2801 }
2802
2803 #[gpui::test]
2804 fn shift_extension_falls_back_when_the_anchor_participant_was_swept(cx: &mut TestAppContext) {
2805 cx.update(|cx| {
2806 let mut selection_state = WindowSelectionState::default();
2807 let first = FakeParticipant::new("first", cx);
2808 let second = FakeParticipant::new("second", cx);
2809 first.register(
2810 &mut selection_state,
2811 0.,
2812 TextSelectionScopeId::default(),
2813 0,
2814 cx,
2815 );
2816 selection_state.begin(point(px(1.), px(1.)), false, cx);
2817 selection_state.update(point(px(8.), px(1.)), cx);
2818 selection_state.end(cx);
2819
2820 selection_state.finish_frame(cx);
2821 selection_state.finish_frame(cx);
2822 second.register(
2823 &mut selection_state,
2824 20.,
2825 TextSelectionScopeId::default(),
2826 1,
2827 cx,
2828 );
2829 selection_state.begin(point(px(1.), px(21.)), true, cx);
2830 selection_state.update(point(px(8.), px(21.)), cx);
2831 selection_state.end(cx);
2832
2833 assert_eq!(selection_state.selected_text(cx), "second");
2834 });
2835 }
2836
2837 #[gpui::test]
2838 fn scope_and_suppression_prevent_unrelated_participants_from_participating(
2839 cx: &mut TestAppContext,
2840 ) {
2841 cx.update(|cx| {
2842 let mut selection_state = WindowSelectionState::default();
2843 let base = FakeParticipant::new("base", cx);
2844 let modal = FakeParticipant::new("modal", cx);
2845 base.register(
2846 &mut selection_state,
2847 0.,
2848 TextSelectionScopeId::default(),
2849 0,
2850 cx,
2851 );
2852 modal.register(&mut selection_state, 20., TextSelectionScopeId(1), 1, cx);
2853
2854 selection_state.set_active_scope(TextSelectionScopeId(1), cx);
2855 selection_state.begin(point(px(1.), px(21.)), false, cx);
2856 selection_state.update(point(px(8.), px(21.)), cx);
2857 selection_state.end(cx);
2858 assert_eq!(selection_state.selected_text(cx), "modal");
2859
2860 selection_state.clear(cx);
2861 GlobalState::init(cx);
2862 GlobalState::suppress_text_selection(cx);
2863 selection_state.begin(point(px(1.), px(21.)), false, cx);
2864 selection_state.update(point(px(8.), px(21.)), cx);
2865 assert!(!selection_state.has_selection(cx));
2866 });
2867 }
2868
2869 #[gpui::test]
2870 fn dead_participants_are_pruned_and_empty_selection_falls_back_safely(cx: &mut TestAppContext) {
2871 let selection_state = cx.update(|cx| {
2872 let selection_state = cx.new(|_| WindowSelectionState::default());
2873 let participant = FakeParticipant::new("gone", cx);
2874 selection_state.update(cx, |selection_state, cx| {
2875 participant.register(selection_state, 0., TextSelectionScopeId::default(), 0, cx)
2876 });
2877 selection_state
2878 });
2879 cx.update(|cx| {
2880 selection_state.update(cx, |selection_state, cx| {
2881 selection_state.begin(point(px(1.), px(1.)), false, cx);
2882 selection_state.update(point(px(8.), px(1.)), cx);
2883 selection_state.end(cx);
2884
2885 assert_eq!(selection_state.selected_text(cx), "");
2886 assert!(!selection_state.has_selection(cx));
2887 });
2888 });
2889 }
2890
2891 #[gpui::test]
2892 fn text_selection_namespace_reports_copies_ends_and_clears_selection(cx: &mut TestAppContext) {
2893 let (view, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
2894 selection: TextSelectionHandle::new("copied", cx),
2895 });
2896 cx.update(|window, cx| {
2897 let selection = view.read(cx).selection.clone();
2898 let selection_state = WindowSelectionState::ensure(window, cx);
2899 selection_state.update(cx, |selection_state, cx| {
2900 FakeParticipant { selection }.register(
2901 selection_state,
2902 0.,
2903 TextSelectionScopeId::default(),
2904 0,
2905 cx,
2906 );
2907 selection_state.begin(point(px(1.), px(1.)), false, cx);
2908 selection_state.update(point(px(8.), px(1.)), cx);
2909 });
2910
2911 assert!(TextSelection::has_selection(window, cx));
2912 assert_eq!(TextSelection::selected_text(window, cx), "copied");
2913 TextSelection::end(window, cx);
2914 assert!(TextSelection::has_selection(window, cx));
2915 TextSelection::clear(window, cx);
2916 assert!(!TextSelection::has_selection(window, cx));
2917 assert_eq!(TextSelection::selected_text(window, cx), "");
2918 });
2919 }
2920
2921 #[gpui::test]
2922 fn two_windows_isolate_selection_copy_clear_and_release_ownership(cx: &mut TestAppContext) {
2923 let first = cx.add_window(|_, cx| WindowOwnedSelectionView {
2924 selection: TextSelectionHandle::new("first", cx),
2925 });
2926 let second = cx.add_window(|_, cx| WindowOwnedSelectionView {
2927 selection: TextSelectionHandle::new("second", cx),
2928 });
2929 let first_selection = cx.update(|cx| first.read(cx).unwrap().selection.clone());
2930 let second_selection = cx.update(|cx| second.read(cx).unwrap().selection.clone());
2931
2932 let first_state = cx
2933 .update_window(*first, |_, window, cx| {
2934 let _ = window.draw(cx);
2935 first_selection.set_local_selection(true, cx);
2936 assert_eq!(TextSelection::selected_text(window, cx), "first");
2937 WindowSelectionState::existing(window, cx)
2938 .unwrap()
2939 .downgrade()
2940 })
2941 .unwrap();
2942 cx.update_window(*second, |_, window, cx| {
2943 let _ = window.draw(cx);
2944 second_selection.set_local_selection(true, cx);
2945 assert_eq!(TextSelection::selected_text(window, cx), "second");
2946 })
2947 .unwrap();
2948
2949 cx.update_window(*first, |_, window, cx| {
2950 TextSelection::clear(window, cx);
2951 assert_eq!(TextSelection::selected_text(window, cx), "");
2952 })
2953 .unwrap();
2954 cx.update_window(*second, |_, window, cx| {
2955 assert_eq!(TextSelection::selected_text(window, cx), "second");
2956 })
2957 .unwrap();
2958
2959 cx.update_window(*first, |_, window, _| window.remove_window())
2960 .unwrap();
2961 cx.run_until_parked();
2962
2963 assert!(first_state.upgrade().is_none());
2964 cx.update_window(*second, |_, window, cx| {
2965 assert_eq!(TextSelection::selected_text(window, cx), "second");
2966 })
2967 .unwrap();
2968 cx.update(|cx| {
2969 assert_eq!(cx.global::<SelectionStateRegistry>().0.len(), 1);
2970 });
2971 }
2972
2973 #[gpui::test]
2974 fn copy_callback_can_reenter_window_and_handle_selection(cx: &mut TestAppContext) {
2975 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
2976 cx.update(|window, cx| {
2977 let _ = window.draw(cx);
2978 let state = WindowSelectionState::existing(window, cx).unwrap();
2979 let selection = TextSelectionHandle::new("fallback", cx);
2980 let state_for_copy = state.clone();
2981 let selection_for_copy = selection.clone();
2982 selection.copy_with(
2983 move |cx: &mut App| {
2984 state_for_copy.update(cx, |state, _| {
2985 assert!(state.snapshot().is_some());
2986 });
2987 assert!(selection_for_copy.snapshot(cx).is_some());
2988 selection_for_copy.set_fallback_copy_text("reentered", cx);
2989 "reentrant copy".to_string()
2990 },
2991 cx,
2992 );
2993 state.update(cx, |state, cx| {
2994 FakeParticipant {
2995 selection: selection.clone(),
2996 }
2997 .register(state, 0., TextSelectionScopeId::default(), 0, cx);
2998 state.begin(point(px(1.), px(1.)), false, cx);
2999 state.update(point(px(8.), px(1.)), cx);
3000 state.end(cx);
3001 });
3002
3003 assert_eq!(TextSelection::selected_text(window, cx), "reentrant copy");
3004 });
3005 }
3006
3007 #[gpui::test]
3008 fn cross_participant_selection_excludes_participants_outside_its_document_interval(
3009 cx: &mut TestAppContext,
3010 ) {
3011 cx.update(|cx| {
3012 let mut selection_state = WindowSelectionState::default();
3013 let first = FakeParticipant::new("first", cx);
3014 let second = FakeParticipant::new("second", cx);
3015 let third = FakeParticipant::new("third", cx);
3016 first.register(
3017 &mut selection_state,
3018 0.,
3019 TextSelectionScopeId::default(),
3020 0,
3021 cx,
3022 );
3023 second.register(
3024 &mut selection_state,
3025 20.,
3026 TextSelectionScopeId::default(),
3027 1,
3028 cx,
3029 );
3030 third.register(
3031 &mut selection_state,
3032 40.,
3033 TextSelectionScopeId::default(),
3034 2,
3035 cx,
3036 );
3037
3038 selection_state.begin(point(px(1.), px(1.)), false, cx);
3039 selection_state.update(point(px(1.), px(25.)), cx);
3040 selection_state.end(cx);
3041
3042 assert_eq!(selection_state.selected_text(cx), "first\nsecond");
3043 assert!(third.selection.snapshot(cx).is_none());
3044 });
3045 }
3046
3047 #[gpui::test]
3048 fn changing_scope_clears_the_previous_scope_selection(cx: &mut TestAppContext) {
3049 cx.update(|cx| {
3050 let mut selection_state = WindowSelectionState::default();
3051 let base = FakeParticipant::new("base", cx);
3052 let modal = FakeParticipant::new("modal", cx);
3053 base.register(
3054 &mut selection_state,
3055 0.,
3056 TextSelectionScopeId::default(),
3057 0,
3058 cx,
3059 );
3060 modal.register(
3061 &mut selection_state,
3062 20.,
3063 TextSelectionScopeId::from_raw(1),
3064 1,
3065 cx,
3066 );
3067
3068 selection_state.begin(point(px(1.), px(1.)), false, cx);
3069 selection_state.update(point(px(8.), px(1.)), cx);
3070 selection_state.end(cx);
3071 selection_state.set_active_scope(TextSelectionScopeId::from_raw(1), cx);
3072
3073 assert!(!selection_state.has_selection(cx));
3074 assert!(base.selection.snapshot(cx).is_none());
3075 });
3076 }
3077
3078 #[gpui::test]
3079 fn blank_only_drag_never_publishes_or_copies_selection(cx: &mut TestAppContext) {
3080 cx.update(|cx| {
3081 let mut selection_state = WindowSelectionState::default();
3082 let participant = FakeParticipant::new("participant", cx);
3083 participant.register(
3084 &mut selection_state,
3085 0.,
3086 TextSelectionScopeId::default(),
3087 0,
3088 cx,
3089 );
3090
3091 selection_state.begin(point(px(200.), px(1.)), false, cx);
3092 selection_state.update(point(px(200.), px(8.)), cx);
3093 selection_state.end(cx);
3094
3095 assert!(!selection_state.has_selection(cx));
3096 assert_eq!(selection_state.selected_text(cx), "");
3097 assert!(participant.selection.snapshot(cx).is_none());
3098 });
3099 }
3100
3101 #[gpui::test]
3102 fn stale_live_participants_are_removed_when_the_next_frame_begins(cx: &mut TestAppContext) {
3103 cx.update(|cx| {
3104 let mut selection_state = WindowSelectionState::default();
3105 let participant = FakeParticipant::new("stale", cx);
3106 participant.register(
3107 &mut selection_state,
3108 0.,
3109 TextSelectionScopeId::default(),
3110 0,
3111 cx,
3112 );
3113 selection_state.begin(point(px(1.), px(1.)), false, cx);
3114 selection_state.update(point(px(8.), px(1.)), cx);
3115 selection_state.end(cx);
3116
3117 selection_state.finish_frame(cx);
3118 selection_state.finish_frame(cx);
3119 assert_eq!(selection_state.selected_text(cx), "");
3120 assert!(participant.selection.snapshot(cx).is_none());
3121 });
3122 }
3123
3124 #[gpui::test]
3125 fn clear_stops_anchor_auto_scroll_before_discarding_the_anchor(cx: &mut TestAppContext) {
3126 let commands = Rc::new(RefCell::new(Vec::new()));
3127 let observed = commands.clone();
3128 let (mut selection_state, participant) = cx.update(|cx| {
3129 let selection_state = WindowSelectionState::default();
3130 let participant = FakeParticipant::new("scroll", cx);
3131 participant
3132 .selection
3133 .subscribe(
3134 move |event, _| {
3135 if let TextSelectionEvent::AutoScroll(delta) = event {
3136 observed.borrow_mut().push(*delta);
3137 }
3138 },
3139 cx,
3140 )
3141 .detach();
3142 (selection_state, participant)
3143 });
3144 cx.run_until_parked();
3145 cx.update(|cx| {
3146 participant.register(
3147 &mut selection_state,
3148 0.,
3149 TextSelectionScopeId::default(),
3150 0,
3151 cx,
3152 );
3153
3154 selection_state.begin(point(px(1.), px(1.)), false, cx);
3155 selection_state.update(point(px(1.), px(25.)), cx);
3156 selection_state.clear(cx);
3157 });
3158 cx.run_until_parked();
3159 assert!(commands.borrow().iter().any(Option::is_some));
3160 assert_eq!(commands.borrow().last(), Some(&None));
3161 }
3162
3163 #[gpui::test]
3164 fn drag_auto_scroll_stops_when_the_content_mask_collapses(cx: &mut TestAppContext) {
3165 let window = cx.add_window(|_, cx| WindowSelectionView {
3166 selection: TextSelectionHandle::new("unused", cx),
3167 });
3168 window
3169 .update(cx, |_, window, cx| {
3170 let state = cx.new(|_| WindowSelectionState::default());
3171 let participant = FakeParticipant::new("participant", cx);
3172 state.update(cx, |state, cx| {
3173 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3174 state.begin(point(px(1.), px(1.)), false, cx);
3175 });
3176 let collapsed = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(0.)));
3179 state.update(cx, |state, cx| {
3180 state.register_participant(
3181 participant.selection.clone(),
3182 TextSelectionRegistration::new(
3183 Hitbox {
3184 id: HitboxId::placeholder(),
3185 bounds: collapsed,
3186 content_mask: ContentMask { bounds: collapsed },
3187 behavior: HitboxBehavior::Normal,
3188 },
3189 collapsed,
3190 )
3191 .with_text_bounds(vec![collapsed]),
3192 cx,
3193 );
3194 state.update_in_window(point(px(1.), px(50.)), window, cx);
3195 assert!(!state.auto_scroll.is_active());
3196 assert!(state.auto_scroll.last_drag_position.is_none());
3197 });
3198 })
3199 .unwrap();
3200 }
3201
3202 #[gpui::test]
3203 fn pointer_moves_after_a_click_do_not_auto_scroll(cx: &mut TestAppContext) {
3204 let window = cx.add_window(|_, cx| WindowSelectionView {
3205 selection: TextSelectionHandle::new("unused", cx),
3206 });
3207 window
3208 .update(cx, |_, window, cx| {
3209 let state = cx.new(|_| WindowSelectionState::default());
3210 let participant = FakeParticipant::new("participant", cx);
3211 state.update(cx, |state, cx| {
3212 participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3213 state.begin(point(px(1.), px(1.)), false, cx);
3215 state.end(cx);
3216 assert!(state.anchor.is_some());
3217
3218 state.update_in_window(point(px(1.), px(50.)), window, cx);
3219 assert!(!state.auto_scroll.is_active());
3220 });
3221 })
3222 .unwrap();
3223 }
3224
3225 #[gpui::test]
3226 fn proxy_endpoints_break_equal_position_ties_by_document_order(cx: &mut TestAppContext) {
3227 cx.update(|cx| {
3228 let mut selection_state = WindowSelectionState::default();
3229 let later = FakeParticipant::new("later", cx);
3230 let earlier = FakeParticipant::new("earlier", cx);
3231 later.register(
3232 &mut selection_state,
3233 0.,
3234 TextSelectionScopeId::default(),
3235 2,
3236 cx,
3237 );
3238 earlier.register(
3239 &mut selection_state,
3240 0.,
3241 TextSelectionScopeId::default(),
3242 1,
3243 cx,
3244 );
3245
3246 selection_state.begin(point(px(1.), px(1.)), false, cx);
3247 selection_state.update(point(px(200.), px(25.)), cx);
3248 let endpoint = selection_state.snapshot().unwrap().cursor();
3249
3250 assert_eq!(endpoint.entity_id(), Some(earlier.selection.entity_id()));
3251 });
3252 }
3253
3254 #[gpui::test]
3255 fn equal_area_hovered_participants_break_ties_by_document_order(cx: &mut TestAppContext) {
3256 cx.update(|cx| {
3257 for _ in 0..64 {
3258 let mut selection_state = WindowSelectionState::default();
3259 let later = FakeParticipant::new("later", cx);
3260 let earliest = FakeParticipant::new("earliest", cx);
3261 let middle = FakeParticipant::new("middle", cx);
3262 later.register(
3263 &mut selection_state,
3264 0.,
3265 TextSelectionScopeId::default(),
3266 30,
3267 cx,
3268 );
3269 earliest.register(
3270 &mut selection_state,
3271 0.,
3272 TextSelectionScopeId::default(),
3273 10,
3274 cx,
3275 );
3276 middle.register(
3277 &mut selection_state,
3278 0.,
3279 TextSelectionScopeId::default(),
3280 20,
3281 cx,
3282 );
3283
3284 selection_state.begin(point(px(1.), px(1.)), false, cx);
3285 selection_state.update(point(px(8.), px(1.)), cx);
3286
3287 assert_eq!(
3288 selection_state.snapshot().unwrap().anchor().entity_id(),
3289 Some(earliest.selection.entity_id())
3290 );
3291 }
3292 });
3293 }
3294
3295 #[gpui::test]
3296 fn text_selection_namespace_is_a_safe_no_op_until_the_element_is_rendered(
3297 cx: &mut TestAppContext,
3298 ) {
3299 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3300 selection: TextSelectionHandle::new("not enabled", cx),
3301 });
3302 cx.update(|window, cx| {
3303 assert!(!TextSelection::has_selection(window, cx));
3304 assert_eq!(TextSelection::selected_text(window, cx), "");
3305 TextSelection::clear(window, cx);
3306 TextSelection::end(window, cx);
3307 assert!(!TextSelection::has_selection(window, cx));
3308 });
3309 }
3310
3311 #[gpui::test]
3312 fn unit_selection_element_supports_scope_and_registration_on_the_first_frame(
3313 cx: &mut TestAppContext,
3314 ) {
3315 let (view, cx) = cx.add_window_view(|_, cx| FirstFrameScopedSelectionView {
3316 selection: TextSelectionHandle::new("first frame", cx),
3317 });
3318 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
3319
3320 cx.update(|window, cx| {
3321 let _ = window.draw(cx);
3322 let state = WindowSelectionState::existing(window, cx).unwrap();
3323 assert_eq!(
3324 state.read(cx).active_scope,
3325 TextSelectionScopeId::from_raw(23)
3326 );
3327 assert!(
3328 state
3329 .read(cx)
3330 .participants
3331 .contains_key(&selection.entity_id())
3332 );
3333 });
3334 }
3335
3336 #[gpui::test]
3337 fn lazy_registration_does_not_enable_queries_without_the_element(cx: &mut TestAppContext) {
3338 let (_, cx) = cx.add_window_view(|_, cx| WindowSelectionView {
3339 selection: TextSelectionHandle::new("registered", cx),
3340 });
3341 cx.update(|window, cx| {
3342 let selection = TextSelectionHandle::new("registered", cx);
3343 selection.set_local_selection(true, cx);
3344 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(20.)));
3345 let hitbox = Hitbox {
3346 id: HitboxId::placeholder(),
3347 bounds,
3348 content_mask: ContentMask { bounds },
3349 behavior: HitboxBehavior::Normal,
3350 };
3351 selection.register(
3352 TextSelectionRegistration::new(hitbox, bounds).with_text_bounds(vec![bounds]),
3353 window,
3354 cx,
3355 );
3356 assert_eq!(TextSelection::selected_text(window, cx), "");
3357 assert!(!TextSelection::has_selection(window, cx));
3358 TextSelection::clear(window, cx);
3359 assert_eq!(TextSelection::selected_text(window, cx), "");
3360 });
3361 }
3362
3363 #[gpui::test]
3364 fn retained_selection_state_releases_and_does_not_resurrect_selection(cx: &mut TestAppContext) {
3365 let (view, cx) = cx.add_window_view(|_, cx| ToggleSelectionElementView {
3366 enabled: true,
3367 selection: TextSelectionHandle::new("local", cx),
3368 });
3369 let selection = cx.update(|_, cx| view.read(cx).selection.clone());
3370 cx.update(|window, cx| {
3371 let _ = window.draw(cx);
3372 selection.set_local_selection(true, cx);
3373 assert!(TextSelection::has_selection(window, cx));
3374
3375 window.simulate_next_frame(cx);
3376 assert!(TextSelection::has_selection(window, cx));
3377 let _ = window.draw(cx);
3378 assert!(TextSelection::has_selection(window, cx));
3379 });
3380 view.update(cx, |view, cx| {
3381 view.enabled = false;
3382 cx.notify();
3383 });
3384 cx.update(|window, cx| {
3385 let _ = window.draw(cx);
3386 });
3387 cx.update(|window, cx| {
3388 window.simulate_next_frame(cx);
3389 });
3390 cx.update(|window, cx| {
3391 window.simulate_next_frame(cx);
3392 });
3393 cx.run_until_parked();
3394 cx.update(|window, cx| {
3395 assert!(!TextSelection::has_selection(window, cx));
3396 assert_eq!(TextSelection::selected_text(window, cx), "");
3397 assert!(!selection.has_local_selection(cx));
3398 TextSelection::clear(window, cx);
3399 });
3400
3401 view.update(cx, |view, cx| {
3402 view.enabled = true;
3403 cx.notify();
3404 });
3405 cx.update(|window, cx| {
3406 let _ = window.draw(cx);
3407 assert!(!TextSelection::has_selection(window, cx));
3408 assert_eq!(TextSelection::selected_text(window, cx), "");
3409 });
3410 }
3411
3412 #[gpui::test]
3413 fn mounted_selection_element_does_not_keep_an_idle_frame_queue_alive(cx: &mut TestAppContext) {
3414 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3415 cx.update(|window, cx| {
3416 let _ = window.draw(cx);
3417 assert_eq!(window.simulate_next_frame(cx), 0);
3418 assert_eq!(window.simulate_next_frame(cx), 0);
3419 assert!(live_text_selection_state(window, cx).is_some());
3420 });
3421 }
3422
3423 #[gpui::test]
3424 fn selection_element_initializes_suppression_and_respects_bubble_suppression(
3425 cx: &mut TestAppContext,
3426 ) {
3427 let (_, cx) = cx.add_window_view(|_, _| SelectionElementOnlyView);
3428 cx.update(|window, cx| {
3429 let _ = window.draw(cx);
3430 });
3431 cx.simulate_mouse_down(
3432 point(px(1.), px(1.)),
3433 MouseButton::Left,
3434 gpui::Modifiers::default(),
3435 );
3436 cx.simulate_mouse_up(
3437 point(px(1.), px(1.)),
3438 MouseButton::Left,
3439 gpui::Modifiers::default(),
3440 );
3441 cx.update(|window, cx| {
3442 assert!(GlobalState::is_text_selection_suppressed(cx));
3443 assert!(!TextSelection::has_selection(window, cx));
3444 });
3445 }
3446
3447 #[gpui::test]
3448 fn frame_sweep_keeps_a_participant_registered_before_the_selection_element_paints(
3449 cx: &mut TestAppContext,
3450 ) {
3451 cx.update(|cx| {
3452 let mut selection_state = WindowSelectionState::default();
3453 let participant = FakeParticipant::new("painted first", cx);
3454 participant.register(
3455 &mut selection_state,
3456 0.,
3457 TextSelectionScopeId::default(),
3458 0,
3459 cx,
3460 );
3461 selection_state.begin(point(px(1.), px(1.)), false, cx);
3462 selection_state.update(point(px(8.), px(1.)), cx);
3463 selection_state.end(cx);
3464
3465 selection_state.finish_frame(cx);
3466
3467 assert_eq!(selection_state.selected_text(cx), "painted first");
3468 assert!(participant.selection.snapshot(cx).is_some());
3469 });
3470 }
3471
3472 #[gpui::test]
3473 fn two_selection_elements_schedule_only_one_post_frame_sweep(cx: &mut TestAppContext) {
3474 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
3475 selection: TextSelectionHandle::new("once", cx),
3476 });
3477 cx.update(|window, cx| {
3478 let selection_state = WindowSelectionState::ensure(window, cx);
3479 let selection = view.read(cx).selection.clone();
3480 selection_state.update(cx, |selection_state, cx| {
3481 FakeParticipant { selection }.register(
3482 selection_state,
3483 0.,
3484 TextSelectionScopeId::default(),
3485 0,
3486 cx,
3487 );
3488 selection_state.begin(point(px(1.), px(1.)), false, cx);
3489 selection_state.update(point(px(8.), px(1.)), cx);
3490 selection_state.end(cx);
3491 });
3492
3493 let _ = window.draw(cx);
3494 window.simulate_next_frame(cx);
3495
3496 let items = selection_state.read(cx).copy_items(cx);
3497 assert_eq!(resolve_copy_items(items, cx), "once");
3498 });
3499 }
3500
3501 #[gpui::test]
3502 fn duplicate_selection_elements_gate_real_pointer_gestures_and_reentrant_clear(
3503 cx: &mut TestAppContext,
3504 ) {
3505 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
3506 selection: TextSelectionHandle::new("once", cx),
3507 });
3508 let clear_count = Rc::new(Cell::new(0));
3509 cx.update(|window, cx| {
3510 let state = WindowSelectionState::ensure(window, cx);
3511 let state_for_clear = state.clone();
3512 let count = clear_count.clone();
3513 let selection = view.read(cx).selection.clone();
3514 selection
3515 .subscribe(
3516 move |event, cx| {
3517 if matches!(event, TextSelectionEvent::Cleared) {
3518 count.set(count.get() + 1);
3519 let _ = state_for_clear.read(cx).snapshot();
3520 }
3521 },
3522 cx,
3523 )
3524 .detach();
3525 let _ = window.draw(cx);
3526 });
3527
3528 cx.simulate_mouse_down(
3529 point(px(10.), px(10.)),
3530 MouseButton::Left,
3531 gpui::Modifiers::default(),
3532 );
3533 cx.simulate_mouse_up(
3534 point(px(10.), px(10.)),
3535 MouseButton::Left,
3536 gpui::Modifiers::default(),
3537 );
3538 cx.simulate_mouse_down(
3539 point(px(70.), px(10.)),
3540 MouseButton::Left,
3541 gpui::Modifiers {
3542 shift: true,
3543 ..Default::default()
3544 },
3545 );
3546 cx.simulate_mouse_up(
3547 point(px(70.), px(10.)),
3548 MouseButton::Left,
3549 gpui::Modifiers::default(),
3550 );
3551 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
3552
3553 cx.simulate_mouse_down(
3554 point(px(15.), px(10.)),
3555 MouseButton::Left,
3556 gpui::Modifiers::default(),
3557 );
3558 cx.simulate_mouse_move(
3559 point(px(85.), px(10.)),
3560 Some(MouseButton::Left),
3561 gpui::Modifiers::default(),
3562 );
3563 cx.simulate_mouse_up(
3564 point(px(85.), px(10.)),
3565 MouseButton::Left,
3566 gpui::Modifiers::default(),
3567 );
3568 cx.update(|window, cx| assert!(TextSelection::has_selection(window, cx)));
3569 assert_eq!(clear_count.get(), 3);
3570 }
3571
3572 #[gpui::test]
3573 fn selection_layer_handles_real_double_and_triple_click_events(cx: &mut TestAppContext) {
3574 let (text, layout) = laid_out_runs(&["alpha beta"], cx).pop().unwrap();
3575 let (view, cx) = cx.add_window_view(|_, cx| DoubleSelectionElementView {
3576 selection: TextSelectionHandle::new("", cx),
3577 });
3578 cx.update(|window, cx| {
3579 let _ = window.draw(cx);
3580 let selection = view.read(cx).selection.clone();
3581 selection.resolve_content_key_with(|_, _| Some(TextSelectionContentKey::new(17)), cx);
3582 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
3583 });
3584
3585 let position = layout.position_for_index(7).unwrap();
3586 cx.simulate_event(MouseDownEvent {
3587 position,
3588 modifiers: gpui::Modifiers::default(),
3589 button: MouseButton::Left,
3590 click_count: 2,
3591 first_mouse: false,
3592 });
3593 cx.simulate_event(MouseUpEvent {
3594 position,
3595 modifiers: gpui::Modifiers::default(),
3596 button: MouseButton::Left,
3597 click_count: 2,
3598 });
3599 cx.update(|window, cx| {
3600 let selection = view.read(cx).selection.clone();
3601 selection.update_runs(&[text_run(0, text.clone(), layout.clone())], cx);
3602 assert_eq!(TextSelection::selected_text(window, cx), "beta");
3603 let snapshot = selection.snapshot(cx).unwrap();
3604 assert_eq!(
3605 snapshot.anchor().content_key(),
3606 Some(TextSelectionContentKey::new(17))
3607 );
3608 assert_eq!(
3609 snapshot.cursor().content_key(),
3610 Some(TextSelectionContentKey::new(17))
3611 );
3612 });
3613
3614 cx.simulate_event(MouseDownEvent {
3615 position,
3616 modifiers: gpui::Modifiers::default(),
3617 button: MouseButton::Left,
3618 click_count: 3,
3619 first_mouse: false,
3620 });
3621 cx.simulate_event(MouseUpEvent {
3622 position,
3623 modifiers: gpui::Modifiers::default(),
3624 button: MouseButton::Left,
3625 click_count: 3,
3626 });
3627 cx.update(|window, cx| {
3628 let selection = view.read(cx).selection.clone();
3629 selection.update_runs(&[text_run(0, text, layout)], cx);
3630 assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
3631 });
3632 }
3633}