assistant2: Suggest recent files and threads as context (#22959)

The context picker will now display up to 6 recent files/threads to add
as a context:

<img
src="https://github.com/user-attachments/assets/80c87bf9-70ad-4e81-ba24-7a624378b991"
width=400>



Note: We decided to use a `ContextMenu` instead of `Picker` for the
initial one since the latter didn't quite fit the design for the
"Recent" section.

Release Notes:

- N/A

---------

Co-authored-by: Danilo <danilo@zed.dev>
Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-01-10 11:26:53 -03:00 committed by GitHub
parent 49198a7961
commit a267911e83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 649 additions and 350 deletions

View file

@ -12,19 +12,11 @@ use settings::Settings;
use std::{rc::Rc, time::Duration};
use theme::ThemeSettings;
enum ContextMenuItem {
pub enum ContextMenuItem {
Separator,
Header(SharedString),
Label(SharedString),
Entry {
toggle: Option<(IconPosition, bool)>,
label: SharedString,
icon: Option<IconName>,
icon_size: IconSize,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
action: Option<Box<dyn Action>>,
disabled: bool,
},
Entry(ContextMenuEntry),
CustomEntry {
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
@ -32,6 +24,86 @@ enum ContextMenuItem {
},
}
impl ContextMenuItem {
pub fn custom_entry(
entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static,
handler: impl Fn(&mut WindowContext) + 'static,
) -> Self {
Self::CustomEntry {
entry_render: Box::new(entry_render),
handler: Rc::new(move |_, cx| handler(cx)),
selectable: true,
}
}
}
pub struct ContextMenuEntry {
toggle: Option<(IconPosition, bool)>,
label: SharedString,
icon: Option<IconName>,
icon_size: IconSize,
icon_position: IconPosition,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
action: Option<Box<dyn Action>>,
disabled: bool,
}
impl ContextMenuEntry {
pub fn new(label: impl Into<SharedString>) -> Self {
ContextMenuEntry {
toggle: None,
label: label.into(),
icon: None,
icon_size: IconSize::Small,
icon_position: IconPosition::Start,
handler: Rc::new(|_, _| {}),
action: None,
disabled: false,
}
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
}
pub fn icon_position(mut self, position: IconPosition) -> Self {
self.icon_position = position;
self
}
pub fn icon_size(mut self, icon_size: IconSize) -> Self {
self.icon_size = icon_size;
self
}
pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self {
self.toggle = Some((toggle_position, toggled));
self
}
pub fn action(mut self, action: Option<Box<dyn Action>>) -> Self {
self.action = action;
self
}
pub fn handler(mut self, handler: impl Fn(&mut WindowContext) + 'static) -> Self {
self.handler = Rc::new(move |_, cx| handler(cx));
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl From<ContextMenuEntry> for ContextMenuItem {
fn from(entry: ContextMenuEntry) -> Self {
ContextMenuItem::Entry(entry)
}
}
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
@ -93,21 +165,32 @@ impl ContextMenu {
self
}
pub fn extend<I: Into<ContextMenuItem>>(mut self, items: impl IntoIterator<Item = I>) -> Self {
self.items.extend(items.into_iter().map(Into::into));
self
}
pub fn item(mut self, item: impl Into<ContextMenuItem>) -> Self {
self.items.push(item.into());
self
}
pub fn entry(
mut self,
label: impl Into<SharedString>,
action: Option<Box<dyn Action>>,
handler: impl Fn(&mut WindowContext) + 'static,
) -> Self {
self.items.push(ContextMenuItem::Entry {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
handler: Rc::new(move |_, cx| handler(cx)),
icon: None,
icon_size: IconSize::Small,
icon_position: IconPosition::End,
action,
disabled: false,
});
}));
self
}
@ -119,15 +202,16 @@ impl ContextMenu {
action: Option<Box<dyn Action>>,
handler: impl Fn(&mut WindowContext) + 'static,
) -> Self {
self.items.push(ContextMenuItem::Entry {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: Some((position, toggled)),
label: label.into(),
handler: Rc::new(move |_, cx| handler(cx)),
icon: None,
icon_size: IconSize::Small,
icon_position: position,
action,
disabled: false,
});
}));
self
}
@ -162,7 +246,7 @@ impl ContextMenu {
}
pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.items.push(ContextMenuItem::Entry {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
@ -174,9 +258,10 @@ impl ContextMenu {
cx.dispatch_action(action.boxed_clone());
}),
icon: None,
icon_position: IconPosition::End,
icon_size: IconSize::Small,
disabled: false,
});
}));
self
}
@ -185,7 +270,7 @@ impl ContextMenu {
label: impl Into<SharedString>,
action: Box<dyn Action>,
) -> Self {
self.items.push(ContextMenuItem::Entry {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
action: Some(action.boxed_clone()),
@ -198,13 +283,14 @@ impl ContextMenu {
}),
icon: None,
icon_size: IconSize::Small,
icon_position: IconPosition::End,
disabled: true,
});
}));
self
}
pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.items.push(ContextMenuItem::Entry {
self.items.push(ContextMenuItem::Entry(ContextMenuEntry {
toggle: None,
label: label.into(),
@ -212,19 +298,20 @@ impl ContextMenu {
handler: Rc::new(move |_, cx| cx.dispatch_action(action.boxed_clone())),
icon: Some(IconName::ArrowUpRight),
icon_size: IconSize::XSmall,
icon_position: IconPosition::End,
disabled: false,
});
}));
self
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let context = self.action_context.as_ref();
if let Some(
ContextMenuItem::Entry {
ContextMenuItem::Entry(ContextMenuEntry {
handler,
disabled: false,
..
}
})
| ContextMenuItem::CustomEntry { handler, .. },
) = self.selected_index.and_then(|ix| self.items.get(ix))
{
@ -304,11 +391,11 @@ impl ContextMenu {
}
if let Some(ix) = self.items.iter().position(|item| {
if let ContextMenuItem::Entry {
if let ContextMenuItem::Entry(ContextMenuEntry {
action: Some(action),
disabled: false,
..
} = item
}) = item
{
action.partial_eq(dispatched)
} else {
@ -346,7 +433,7 @@ impl ContextMenuItem {
ContextMenuItem::Header(_)
| ContextMenuItem::Separator
| ContextMenuItem::Label { .. } => false,
ContextMenuItem::Entry { disabled, .. } => !disabled,
ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled,
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
}
}
@ -356,12 +443,17 @@ impl Render for ContextMenu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
div().occlude().elevation_2(cx).flex().flex_row().child(
WithRemSize::new(ui_font_size).flex().child(
WithRemSize::new(ui_font_size)
.occlude()
.elevation_2(cx)
.flex()
.flex_row()
.child(
v_flex()
.id("context-menu")
.min_w(px(200.))
.max_h(vh(0.75, cx))
.flex_1()
.overflow_y_scroll()
.track_focus(&self.focus_handle(cx))
.on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
@ -374,11 +466,11 @@ impl Render for ContextMenu {
.on_action(cx.listener(ContextMenu::cancel))
.when(!self.delayed, |mut el| {
for item in self.items.iter() {
if let ContextMenuItem::Entry {
if let ContextMenuItem::Entry(ContextMenuEntry {
action: Some(action),
disabled: false,
..
} = item
}) = item
{
el = el.on_boxed_action(
&**action,
@ -388,7 +480,6 @@ impl Render for ContextMenu {
}
el
})
.flex_none()
.child(List::new().children(self.items.iter_mut().enumerate().map(
|(ix, item)| {
match item {
@ -403,15 +494,16 @@ impl Render for ContextMenu {
.disabled(true)
.child(Label::new(label.clone()))
.into_any_element(),
ContextMenuItem::Entry {
ContextMenuItem::Entry(ContextMenuEntry {
toggle,
label,
handler,
icon,
icon_size,
icon_position,
action,
disabled,
} => {
}) => {
let handler = handler.clone();
let menu = cx.view().downgrade();
let color = if *disabled {
@ -422,10 +514,21 @@ impl Render for ContextMenu {
let label_element = if let Some(icon_name) = icon {
h_flex()
.gap_1()
.when(*icon_position == IconPosition::Start, |flex| {
flex.child(
Icon::new(*icon_name)
.size(*icon_size)
.color(color),
)
})
.child(Label::new(label.clone()).color(color))
.child(
Icon::new(*icon_name).size(*icon_size).color(color),
)
.when(*icon_position == IconPosition::End, |flex| {
flex.child(
Icon::new(*icon_name)
.size(*icon_size)
.color(color),
)
})
.into_any_element()
} else {
Label::new(label.clone()).color(color).into_any_element()
@ -520,7 +623,6 @@ impl Render for ContextMenu {
}
},
))),
),
)
)
}
}

View file

@ -1,6 +1,7 @@
use gpui::{
div, AnyElement, Bounds, Div, DivFrameState, Element, ElementId, GlobalElementId, Hitbox,
IntoElement, LayoutId, ParentElement, Pixels, StyleRefinement, Styled, WindowContext,
InteractiveElement as _, IntoElement, LayoutId, ParentElement, Pixels, StyleRefinement, Styled,
WindowContext,
};
/// An element that sets a particular rem size for its children.
@ -18,6 +19,13 @@ impl WithRemSize {
rem_size: rem_size.into(),
}
}
/// Block the mouse from interacting with this element or any of its children
/// The fluent API equivalent to [`Interactivity::occlude_mouse`]
pub fn occlude(mut self) -> Self {
self.div = self.div.occlude();
self
}
}
impl Styled for WithRemSize {
@ -37,7 +45,7 @@ impl Element for WithRemSize {
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
self.div.id()
Element::id(&self.div)
}
fn request_layout(