assistant2: Highlight crease on selection (#24358)

João Marcos , Piotr , and Danilo Leal created

Give the inline file crease inside of `assistant2`'s editor a
selection background when there is a selection over it

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/assistant2/src/context_picker/file_context_picker.rs | 63 +++++-
crates/assistant_context_editor/src/context_editor.rs       | 10 
crates/editor/src/display_map/fold_map.rs                   | 10 
crates/editor/src/editor.rs                                 |  2 
crates/multi_buffer/src/anchor.rs                           |  9 
5 files changed, 70 insertions(+), 24 deletions(-)

Detailed changes

crates/assistant2/src/context_picker/file_context_picker.rs 🔗

@@ -7,19 +7,19 @@ use std::sync::Arc;
 use editor::actions::FoldAt;
 use editor::display_map::{Crease, FoldId};
 use editor::scroll::Autoscroll;
-use editor::{Anchor, Editor, FoldPlaceholder, ToPoint};
+use editor::{Anchor, AnchorRangeExt, Editor, FoldPlaceholder, ToPoint};
 use file_icons::FileIcons;
 use fuzzy::PathMatch;
 use gpui::{
-    AnyElement, App, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful, Task,
-    WeakEntity,
+    AnyElement, App, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, Stateful,
+    Task, WeakEntity,
 };
 use multi_buffer::{MultiBufferPoint, MultiBufferRow};
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
 use rope::Point;
 use text::SelectionGoal;
-use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex, ListItem, Tooltip};
+use ui::{prelude::*, ButtonLike, Disclosure, ListItem, TintColor, Tooltip};
 use util::ResultExt as _;
 use workspace::{notifications::NotifyResultExt, Workspace};
 
@@ -238,11 +238,11 @@ impl PickerDelegate for FileContextPickerDelegate {
             path: mat.path.clone(),
         };
 
-        let Some(editor) = self.editor.upgrade() else {
+        let Some(editor_entity) = self.editor.upgrade() else {
             return;
         };
 
-        editor.update(cx, |editor, cx| {
+        editor_entity.update(cx, |editor, cx| {
             editor.transact(window, cx, |editor, window, cx| {
                 // Move empty selections left by 1 column to select the `@`s, so they get overwritten when we insert.
                 {
@@ -292,7 +292,11 @@ impl PickerDelegate for FileContextPickerDelegate {
                     .unwrap_or_else(|| SharedString::new(""));
 
                 let placeholder = FoldPlaceholder {
-                    render: render_fold_icon_button(file_icon, file_name.into()),
+                    render: render_fold_icon_button(
+                        file_icon,
+                        file_name.into(),
+                        editor_entity.downgrade(),
+                    ),
                     ..Default::default()
                 };
 
@@ -464,11 +468,50 @@ pub fn render_file_context_entry(
 fn render_fold_icon_button(
     icon: SharedString,
     label: SharedString,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut Window, &mut App) -> AnyElement> {
-    Arc::new(move |fold_id, _fold_range, _window, _cx| {
+    editor: WeakEntity<Editor>,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+    Arc::new(move |fold_id, fold_range, cx| {
+        let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
+            editor.update(cx, |editor, cx| {
+                let snapshot = editor
+                    .buffer()
+                    .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
+
+                let is_in_pending_selection = || {
+                    editor
+                        .selections
+                        .pending
+                        .as_ref()
+                        .is_some_and(|pending_selection| {
+                            pending_selection
+                                .selection
+                                .range()
+                                .includes(&fold_range, &snapshot)
+                        })
+                };
+
+                let mut is_in_complete_selection = || {
+                    editor
+                        .selections
+                        .disjoint_in_range::<usize>(fold_range.clone(), cx)
+                        .into_iter()
+                        .any(|selection| {
+                            // This is needed to cover a corner case, if we just check for an existing
+                            // selection in the fold range, having a cursor at the start of the fold
+                            // marks it as selected. Non-empty selections don't cause this.
+                            let length = selection.end - selection.start;
+                            length > 0
+                        })
+                };
+
+                is_in_pending_selection() || is_in_complete_selection()
+            })
+        });
+
         ButtonLike::new(fold_id)
             .style(ButtonStyle::Filled)
-            .layer(ElevationIndex::ElevatedSurface)
+            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+            .toggle_state(is_in_text_selection)
             .child(
                 h_flex()
                     .gap_1()

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -634,7 +634,7 @@ impl ContextEditor {
                                 }
                             });
                             let placeholder = FoldPlaceholder {
-                                render: Arc::new(move |_, _, _, _| Empty.into_any()),
+                                render: Arc::new(move |_, _, _| Empty.into_any()),
                                 ..Default::default()
                             };
                             let render_toggle = {
@@ -2668,8 +2668,8 @@ fn render_fold_icon_button(
     editor: WeakEntity<Editor>,
     icon: IconName,
     label: SharedString,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut Window, &mut App) -> AnyElement> {
-    Arc::new(move |fold_id, fold_range, _window, _cx| {
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+    Arc::new(move |fold_id, fold_range, _cx| {
         let editor = editor.clone();
         ButtonLike::new(fold_id)
             .style(ButtonStyle::Filled)
@@ -2729,7 +2729,7 @@ pub fn fold_toggle(
 fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
     FoldPlaceholder {
         render: Arc::new({
-            move |fold_id, fold_range, _window, _cx| {
+            move |fold_id, fold_range, _cx| {
                 let editor = editor.clone();
                 ButtonLike::new(fold_id)
                     .style(ButtonStyle::Filled)
@@ -3413,7 +3413,7 @@ fn invoked_slash_command_fold_placeholder(
     FoldPlaceholder {
         constrain_width: false,
         merge_adjacent: false,
-        render: Arc::new(move |fold_id, _, _window, cx| {
+        render: Arc::new(move |fold_id, _, cx| {
             let Some(context) = context.upgrade() else {
                 return Empty.into_any();
             };

crates/editor/src/display_map/fold_map.rs 🔗

@@ -2,7 +2,7 @@ use super::{
     inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
     Highlights,
 };
-use gpui::{AnyElement, App, ElementId, Window};
+use gpui::{AnyElement, App, ElementId};
 use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
 use multi_buffer::{
     Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset,
@@ -21,8 +21,7 @@ use util::post_inc;
 #[derive(Clone)]
 pub struct FoldPlaceholder {
     /// Creates an element to represent this fold's placeholder.
-    pub render:
-        Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut Window, &mut App) -> AnyElement>,
+    pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement>,
     /// If true, the element is constrained to the shaped width of an ellipsis.
     pub constrain_width: bool,
     /// If true, merges the fold with an adjacent one.
@@ -34,7 +33,7 @@ pub struct FoldPlaceholder {
 impl Default for FoldPlaceholder {
     fn default() -> Self {
         Self {
-            render: Arc::new(|_, _, _, _| gpui::Empty.into_any_element()),
+            render: Arc::new(|_, _, _| gpui::Empty.into_any_element()),
             constrain_width: true,
             merge_adjacent: true,
             type_tag: None,
@@ -46,7 +45,7 @@ impl FoldPlaceholder {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test() -> Self {
         Self {
-            render: Arc::new(|_id, _range, _window, _cx| gpui::Empty.into_any_element()),
+            render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
             constrain_width: true,
             merge_adjacent: true,
             type_tag: None,
@@ -486,7 +485,6 @@ impl FoldMap {
                                             (fold.placeholder.render)(
                                                 fold_id,
                                                 fold.range.0.clone(),
-                                                cx.window,
                                                 cx.context,
                                             )
                                         }),

crates/editor/src/editor.rs 🔗

@@ -1142,7 +1142,7 @@ impl Editor {
         let editor = cx.entity().downgrade();
         let fold_placeholder = FoldPlaceholder {
             constrain_width: true,
-            render: Arc::new(move |fold_id, fold_range, _, cx| {
+            render: Arc::new(move |fold_id, fold_range, cx| {
                 let editor = editor.clone();
                 div()
                     .id(fold_id)

crates/multi_buffer/src/anchor.rs 🔗

@@ -189,8 +189,9 @@ impl ToPoint for Anchor {
 }
 
 pub trait AnchorRangeExt {
-    fn cmp(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
-    fn overlaps(&self, b: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
+    fn cmp(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
+    fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
+    fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool;
     fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
     fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
 }
@@ -203,6 +204,10 @@ impl AnchorRangeExt for Range<Anchor> {
         }
     }
 
+    fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
+        self.start.cmp(&other.start, &buffer).is_le() && other.end.cmp(&self.end, &buffer).is_le()
+    }
+
     fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
         self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le()
     }