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