Skip to main content

gpui_base/
text_selection.rs

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/// An opaque selection layer identifier.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
21pub struct TextSelectionScopeId(u64);
22
23impl TextSelectionScopeId {
24    /// Allocates a process-unique scope identifier.
25    ///
26    /// Keep the returned identifier for the semantic lifetime of the scope;
27    /// do not allocate a new identifier on every frame.
28    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/// Stable participant-defined identity for virtualized participant content.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub struct TextSelectionContentKey(u64);
47
48impl TextSelectionContentKey {
49    /// Creates a key from a participant-defined stable content identity.
50    pub const fn new(value: u64) -> Self {
51        Self(value)
52    }
53
54    /// Returns the participant-defined value.
55    pub const fn value(self) -> u64 {
56        self.0
57    }
58}
59
60/// A selection endpoint anchored to a participant's content coordinates.
61#[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    /// Creates an endpoint at a participant-relative content point.
70    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    /// Sets participant-defined endpoint metadata.
79    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    /// Returns the participant which owns this endpoint, when it hit one.
85    pub const fn entity_id(&self) -> Option<EntityId> {
86        self.entity_id
87    }
88
89    /// Returns the participant-relative content point.
90    pub const fn content_point(&self) -> Point<Pixels> {
91        self.point
92    }
93
94    /// Returns participant-defined endpoint metadata captured when it hit a participant.
95    pub const fn content_key(&self) -> Option<TextSelectionContentKey> {
96        self.content_key
97    }
98}
99
100/// Window-coordinate anchor and cursor points for painting a selection.
101#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct TextSelectionWindowPoints {
103    anchor: Point<Pixels>,
104    cursor: Point<Pixels>,
105}
106
107impl TextSelectionWindowPoints {
108    /// Returns the stable anchor in window coordinates.
109    pub const fn anchor(&self) -> Point<Pixels> {
110        self.anchor
111    }
112
113    /// Returns the moving cursor in window coordinates.
114    pub const fn cursor(&self) -> Point<Pixels> {
115        self.cursor
116    }
117}
118
119/// Participant-relative selection endpoints with an optional rendering projection.
120#[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/// How much of one participant participates in a window selection.
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub enum TextSelectionCoverage {
132    /// Only the interval between this participant's two endpoints is selected.
133    #[default]
134    Bounded,
135    /// The participant is selected from its beginning through its endpoint.
136    FromStart,
137    /// The participant is selected from its endpoint through its end.
138    ToEnd,
139    /// The entire participant lies between endpoints in other participants.
140    Full,
141}
142
143impl TextSelectionSnapshot {
144    /// Creates a snapshot from stable participant-relative endpoints.
145    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    /// Sets whether the pointer gesture is still active.
156    pub(crate) const fn with_selecting(mut self, is_selecting: bool) -> Self {
157        self.is_selecting = is_selecting;
158        self
159    }
160
161    /// Sets the current window-coordinate rendering projection.
162    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    /// Sets the portion of the receiving participant covered by this selection.
171    #[cfg(test)]
172    pub(crate) const fn with_coverage(mut self, coverage: TextSelectionCoverage) -> Self {
173        self.coverage = coverage;
174        self
175    }
176
177    /// Returns the stable anchor endpoint.
178    pub const fn anchor(&self) -> TextSelectionEndpoint {
179        self.anchor
180    }
181
182    /// Returns the moving cursor endpoint.
183    pub const fn cursor(&self) -> TextSelectionEndpoint {
184        self.cursor
185    }
186
187    /// Returns whether the pointer gesture is still active.
188    pub const fn is_selecting(&self) -> bool {
189        self.is_selecting
190    }
191
192    /// Returns the window-coordinate endpoints for participants that need them.
193    pub const fn window_points(&self) -> Option<TextSelectionWindowPoints> {
194        self.window_points
195    }
196
197    /// Returns the portion of the receiving participant covered by this selection.
198    pub const fn coverage(&self) -> TextSelectionCoverage {
199        self.coverage
200    }
201}
202
203/// Per-frame geometry reported by a [`TextSelectionHandle`] participant.
204pub 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    /// Creates a registration with default scope, order, and scroll offset.
215    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    /// Sets the participant's content scroll offset.
227    pub fn with_scroll_offset(mut self, scroll_offset: Point<Pixels>) -> Self {
228        self.scroll_offset = scroll_offset;
229        self
230    }
231
232    /// Sets the opaque selection scope.
233    pub fn with_scope(mut self, scope: TextSelectionScopeId) -> Self {
234        self.scope = scope;
235        self
236    }
237
238    /// Sets the stable logical document order.
239    pub fn with_document_order(mut self, document_order: u64) -> Self {
240        self.document_order = document_order;
241        self
242    }
243
244    /// Sets the glyph-bearing bounds used to reject blank-only gestures.
245    pub fn with_text_bounds(mut self, text_bounds: Vec<Bounds<Pixels>>) -> Self {
246        self.text_bounds = text_bounds;
247        self
248    }
249
250    /// Returns the participant hitbox.
251    pub fn hitbox(&self) -> &Hitbox {
252        &self.hitbox
253    }
254
255    /// Returns the participant's window-coordinate bounds.
256    pub const fn bounds(&self) -> Bounds<Pixels> {
257        self.bounds
258    }
259
260    /// Returns the participant's content scroll offset.
261    pub const fn scroll_offset(&self) -> Point<Pixels> {
262        self.scroll_offset
263    }
264
265    /// Returns the opaque selection scope.
266    pub const fn scope(&self) -> TextSelectionScopeId {
267        self.scope
268    }
269
270    /// Returns the stable logical document order.
271    pub const fn document_order(&self) -> u64 {
272        self.document_order
273    }
274
275    /// Returns the glyph-bearing bounds used to reject blank-only gestures.
276    pub fn text_bounds(&self) -> &[Bounds<Pixels>] {
277        &self.text_bounds
278    }
279}
280
281/// Laid-out text reported by a plain selection participant during paint.
282#[derive(Clone)]
283pub struct TextSelectionRun {
284    /// Logical order within the containing participant.
285    document_order: u64,
286    /// The exact text used to produce `layout`.
287    text: SharedString,
288    /// Laid-out glyph geometry in window coordinates.
289    layout: TextLayout,
290    /// The run's window-coordinate paint bounds.
291    bounds: Bounds<Pixels>,
292}
293
294impl TextSelectionRun {
295    /// Creates a laid-out text run.
296    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    /// Sets the run's logical order within the participant.
306    pub const fn with_document_order(mut self, document_order: u64) -> Self {
307        self.document_order = document_order;
308        self
309    }
310
311    /// Returns the run's logical order within its participant.
312    pub const fn document_order(&self) -> u64 {
313        self.document_order
314    }
315
316    /// Returns the exact text used to produce the layout.
317    pub fn text(&self) -> &SharedString {
318        &self.text
319    }
320
321    /// Returns the laid-out glyph geometry.
322    pub fn layout(&self) -> &TextLayout {
323        &self.layout
324    }
325
326    /// Returns the run's window-coordinate paint bounds.
327    pub const fn bounds(&self) -> Bounds<Pixels> {
328        self.bounds
329    }
330}
331
332/// Selection projected onto a participant's laid-out text runs.
333#[derive(Clone, Debug, Default, PartialEq, Eq)]
334pub struct TextSelectionProjection {
335    /// Selected UTF-8 byte ranges paired with the input runs.
336    ranges: Vec<Option<Range<usize>>>,
337    /// Whether the participant participates in the current selection.
338    is_active: bool,
339}
340
341impl TextSelectionProjection {
342    /// Returns selected UTF-8 byte ranges paired with the input runs.
343    pub fn ranges(&self) -> &[Option<Range<usize>>] {
344        &self.ranges
345    }
346
347    /// Returns whether the participant participates in the selection.
348    pub const fn is_active(&self) -> bool {
349        self.is_active
350    }
351}
352
353/// Projects a participant selection snapshot onto laid-out plain-text runs.
354///
355/// The returned states retain the input order so callers can pair every state
356/// with its run. The ranges are always character boundaries; `order` is used
357/// only when a participant caches selected text for copying.
358fn 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/// Notifications emitted by a text-selection participant.
487#[derive(Clone, Copy, Debug, PartialEq)]
488pub enum TextSelectionEvent {
489    /// The participant's window-selection projection changed.
490    SelectionChanged(Option<TextSelectionSnapshot>),
491    /// The active drag requests vertical auto-scroll, or `None` to stop.
492    AutoScroll(Option<Pixels>),
493    /// Window selection cleared the participant's participant-local state.
494    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    /// The current geometry selection snapshot for this participant.
553    fn snapshot(&self) -> Option<TextSelectionSnapshot> {
554        self.snapshot
555    }
556
557    /// Sets the text copied by this participant when it participates in selection.
558    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    /// Marks participant-local selection (for example select-all) as active.
564    fn set_local_selection(&mut self, active: bool) {
565        self.local_selection = active;
566    }
567
568    /// Projects this participant's current snapshot onto plain-text runs and caches
569    /// their selected substrings for the window selection query.
570    ///
571    /// Call this once per painted run. A snapshot change or
572    /// Clearing window selection invalidates the cache immediately, so copy
573    /// never returns text from a previous projection while waiting to repaint.
574    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    /// Installs the callback which focuses the participant when a drag begins in it.
600    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    /// Installs a participant-specific copy projection.
609    fn copy_with(&mut self, callback: impl Fn(&mut App) -> String + 'static) {
610        self.copy = Some(Rc::new(callback));
611    }
612
613    /// Installs a participant-specific lookup for stable virtualized content keys.
614    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/// A stable, participant-neutral handle for text that participates in window selection.
662#[derive(Clone)]
663pub struct TextSelectionHandle(Entity<SelectableTextState>);
664
665impl TextSelectionHandle {
666    /// Creates a selection participant handle with fallback text for copying.
667    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    /// Returns this participant's stable identity.
672    pub fn entity_id(&self) -> EntityId {
673        self.0.entity_id()
674    }
675
676    /// Returns the current geometry selection snapshot for this participant.
677    pub fn snapshot(&self, cx: &App) -> Option<TextSelectionSnapshot> {
678        self.0.read(cx).snapshot()
679    }
680
681    /// Sets the fallback text copied while this participant participates.
682    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    /// Marks participant-local selection, such as select-all, as active.
688    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    /// Returns whether participant-local selection is active.
694    pub fn has_local_selection(&self, cx: &App) -> bool {
695        self.0.read(cx).local_selection
696    }
697
698    /// Registers this participant and its geometry for the current frame.
699    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    /// Projects the current snapshot onto plain-text runs and caches their copy text.
717    pub fn update_runs(&self, runs: &[TextSelectionRun], cx: &mut App) -> TextSelectionProjection {
718        self.0.update(cx, |state, _| state.update_runs(runs))
719    }
720
721    /// Subscribes to participant selection notifications.
722    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    /// Subscribes `window` to refresh whenever this participant's selection changes.
731    #[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    /// Sets the callback which focuses the participant when a drag begins in it.
745    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    /// Sets the synchronous participant cleanup command used by window clear.
751    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    /// Sets a participant-specific copy projection.
756    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    /// Sets a participant-specific lookup for stable virtualized content keys.
761    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/// Window-local generic text-selection state.
824#[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    /// Updates the active scope. Participants from other scopes cannot participate.
934    #[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    /// Sweeps participants after a rendered frame has completed.
955    ///
956    /// Registrations are stamped with the current generation while any sibling
957    /// is painting. Sweeping only after paint makes registration independent of
958    /// whether a participant or the lifecycle element paints first.
959    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    /// Registers this frame's geometry for a participant.
992    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    /// Starts a selection gesture using bounds hit testing (useful to adapters/tests).
1011    #[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    /// Updates the current gesture using bounds hit testing.
1017    #[cfg(test)]
1018    fn update(&mut self, position: Point<Pixels>, cx: &mut App) {
1019        self.update_impl(position, None, cx);
1020    }
1021
1022    /// Ends the current gesture and keeps its selection visible.
1023    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    /// Clears both window selection and every participant's local selection.
1038    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    /// Returns whether a drag or a participant-local selection is active.
1076    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    /// Returns the current resolved selection endpoints.
1087    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    /// Returns whether a drag is currently in progress.
1103    #[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 = &registration.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(&registration.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        // The content mask is the nearest clipping viewport established by a
1471        // scrollable ancestor. It remains stable as the participant itself
1472        // moves, so selection keeps scrolling the same related region even
1473        // after the anchor text has moved out of view.
1474        let visible_bounds = registration.registration.hitbox.content_mask.bounds;
1475        // Keeps the synthesized wheel event hit-testing inside the mask.
1476        const HIT_TEST_INSET: Pixels = px(1.);
1477        // A collapsed mask leaves an empty clamp range below — stop.
1478        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)]
1558/// Non-owning window locator; retained [`TextSelection`] element state owns
1559/// each live selection entity.
1560struct 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
1625/// Window-level operations for text selection.
1626pub struct TextSelection;
1627
1628impl TextSelection {
1629    /// Returns the currently selected text in logical document order.
1630    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    /// Returns whether the window has a geometry selection or any participant
1639    /// has an active participant-local selection such as select-all.
1640    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    /// Clears window selection and every participant's local selection.
1645    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    /// Clears selection for a known window identifier.
1653    ///
1654    /// Prefer [`Self::clear`] when a window reference is available. This
1655    /// narrow entry point supports hosts retiring deprecated window wrappers.
1656    pub fn clear_for_window(window_id: gpui::WindowId, cx: &mut App) {
1657        clear_window_text_selection(window_id, cx);
1658    }
1659
1660    /// Ends the current drag while leaving its selection visible.
1661    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    /// Activates the opaque selection scope for this window.
1668    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
1683/// A zero-sized root layer which enables text selection for a window.
1684///
1685/// Mount one as the root's first child. Its stable `"window-text-selection"`
1686/// element identity retains the window-local selection entity across frames.
1687pub 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        // Automatic participant order is paint order within this frame. Keep
1821        // this lifecycle in base so base-only applications do not need a
1822        // separate root component to reset it. Otherwise, registering the
1823        // first of two selected TextViews temporarily reverses their order
1824        // against the previous frame and alternates coverage forever.
1825        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                // The scrollable ancestor got clipped away mid-drag, so the
3121                // refreshed registration carries a collapsed content mask.
3122                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}