Allow to reuse PickerPopoverMenu outside of the model selector (#31684)

LSP button preparation step: move out the component that will be used to
build the button's context menu.

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-05-29 15:55:47 +03:00 committed by GitHub
parent 45f9edcbb9
commit f792827a01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 243 additions and 235 deletions

View file

@ -1,10 +1,11 @@
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use fs::Fs; use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString}; use gpui::{Entity, FocusHandle, SharedString};
use picker::popover_menu::PickerPopoverMenu;
use crate::Thread; use crate::Thread;
use assistant_context_editor::language_model_selector::{ use assistant_context_editor::language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector, LanguageModelSelector, ToggleModelSelector, language_model_selector,
}; };
use language_model::{ConfiguredModel, LanguageModelRegistry}; use language_model::{ConfiguredModel, LanguageModelRegistry};
use settings::update_settings_file; use settings::update_settings_file;
@ -35,7 +36,7 @@ impl AgentModelSelector {
Self { Self {
selector: cx.new(move |cx| { selector: cx.new(move |cx| {
let fs = fs.clone(); let fs = fs.clone();
LanguageModelSelector::new( language_model_selector(
{ {
let model_type = model_type.clone(); let model_type = model_type.clone();
move |cx| match &model_type { move |cx| match &model_type {
@ -100,15 +101,14 @@ impl AgentModelSelector {
} }
impl Render for AgentModelSelector { impl Render for AgentModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone(); let focus_handle = self.focus_handle.clone();
let model = self.selector.read(cx).active_model(cx); let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model let model_name = model
.map(|model| model.model.name().0) .map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected")); .unwrap_or_else(|| SharedString::from("No model selected"));
PickerPopoverMenu::new(
LanguageModelSelectorPopoverMenu::new(
self.selector.clone(), self.selector.clone(),
Button::new("active-model", model_name) Button::new("active-model", model_name)
.label_size(LabelSize::Small) .label_size(LabelSize::Small)
@ -127,7 +127,9 @@ impl Render for AgentModelSelector {
) )
}, },
gpui::Corner::BottomRight, gpui::Corner::BottomRight,
cx,
) )
.with_handle(self.menu_handle.clone()) .with_handle(self.menu_handle.clone())
.render(window, cx)
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
language_model_selector::{ language_model_selector::{
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector, LanguageModelSelector, ToggleModelSelector, language_model_selector,
}, },
max_mode_tooltip::MaxModeTooltip, max_mode_tooltip::MaxModeTooltip,
}; };
@ -43,7 +43,7 @@ use language_model::{
Role, Role,
}; };
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use picker::Picker; use picker::{Picker, popover_menu::PickerPopoverMenu};
use project::{Project, Worktree}; use project::{Project, Worktree};
use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate}; use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
use rope::Point; use rope::Point;
@ -283,7 +283,7 @@ impl ContextEditor {
slash_menu_handle: Default::default(), slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(), dragged_file_worktrees: Vec::new(),
language_model_selector: cx.new(|cx| { language_model_selector: cx.new(|cx| {
LanguageModelSelector::new( language_model_selector(
|cx| LanguageModelRegistry::read_global(cx).default_model(), |cx| LanguageModelRegistry::read_global(cx).default_model(),
move |model, cx| { move |model, cx| {
update_settings_file::<AgentSettings>( update_settings_file::<AgentSettings>(
@ -2100,7 +2100,11 @@ impl ContextEditor {
) )
} }
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement { fn render_language_model_selector(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx) let active_model = LanguageModelRegistry::read_global(cx)
.default_model() .default_model()
.map(|default| default.model); .map(|default| default.model);
@ -2110,7 +2114,7 @@ impl ContextEditor {
None => SharedString::from("No model selected"), None => SharedString::from("No model selected"),
}; };
LanguageModelSelectorPopoverMenu::new( PickerPopoverMenu::new(
self.language_model_selector.clone(), self.language_model_selector.clone(),
ButtonLike::new("active-model") ButtonLike::new("active-model")
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
@ -2138,8 +2142,10 @@ impl ContextEditor {
) )
}, },
gpui::Corner::BottomLeft, gpui::Corner::BottomLeft,
cx,
) )
.with_handle(self.language_model_selector_menu_handle.clone()) .with_handle(self.language_model_selector_menu_handle.clone())
.render(window, cx)
} }
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> { fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
@ -2615,7 +2621,7 @@ impl Render for ContextEditor {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child(self.render_language_model_selector(cx)) .child(self.render_language_model_selector(window, cx))
.child(self.render_send_button(window, cx)), .child(self.render_send_button(window, cx)),
), ),
) )

View file

@ -4,8 +4,7 @@ use collections::{HashSet, IndexMap};
use feature_flags::ZedProFeatureFlag; use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{ use gpui::{
Action, AnyElement, AnyView, App, BackgroundExecutor, Corner, DismissEvent, Entity, Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task,
EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
action_with_deprecated_aliases, action_with_deprecated_aliases,
}; };
use language_model::{ use language_model::{
@ -15,7 +14,7 @@ use language_model::{
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use proto::Plan; use proto::Plan;
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*}; use ui::{ListItem, ListItemSpacing, prelude::*};
action_with_deprecated_aliases!( action_with_deprecated_aliases!(
agent, agent,
@ -31,77 +30,146 @@ const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>; type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>; type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
pub struct LanguageModelSelector { pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
picker: Entity<Picker<LanguageModelPickerDelegate>>,
pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
window: &mut Window,
cx: &mut Context<LanguageModelSelector>,
) -> LanguageModelSelector {
let delegate = LanguageModelPickerDelegate::new(get_active_model, on_model_changed, window, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
}
fn all_models(cx: &App) -> GroupedModels {
let mut recommended = Vec::new();
let mut recommended_set = HashSet::default();
for provider in LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
{
let models = provider.recommended_models(cx);
recommended_set.extend(models.iter().map(|model| (model.provider_id(), model.id())));
recommended.extend(
provider
.recommended_models(cx)
.into_iter()
.map(move |model| ModelInfo {
model: model.clone(),
icon: provider.icon(),
}),
);
}
let other_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| {
(
provider.id(),
provider
.provided_models(cx)
.into_iter()
.filter_map(|model| {
let not_included =
!recommended_set.contains(&(model.provider_id(), model.id()));
not_included.then(|| ModelInfo {
model: model.clone(),
icon: provider.icon(),
})
})
.collect::<Vec<_>>(),
)
})
.collect::<IndexMap<_, _>>();
GroupedModels {
recommended,
other: other_models,
}
}
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
}
pub struct LanguageModelPickerDelegate {
on_model_changed: OnModelChanged,
get_active_model: GetActiveModel,
all_models: Arc<GroupedModels>,
filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize,
_authenticate_all_providers_task: Task<()>, _authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
impl LanguageModelSelector { impl LanguageModelPickerDelegate {
pub fn new( fn new(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static, get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static, on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Picker<Self>>,
) -> Self { ) -> Self {
let on_model_changed = Arc::new(on_model_changed); let on_model_changed = Arc::new(on_model_changed);
let models = all_models(cx);
let entries = models.entries();
let all_models = Self::all_models(cx); Self {
let entries = all_models.entries();
let delegate = LanguageModelPickerDelegate {
language_model_selector: cx.entity().downgrade(),
on_model_changed: on_model_changed.clone(), on_model_changed: on_model_changed.clone(),
all_models: Arc::new(all_models), all_models: Arc::new(models),
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries, filtered_entries: entries,
get_active_model: Arc::new(get_active_model), get_active_model: Arc::new(get_active_model),
};
let picker = cx.new(|cx| {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
});
let subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
LanguageModelSelector {
picker,
_authenticate_all_providers_task: Self::authenticate_all_providers(cx), _authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![ _subscriptions: vec![cx.subscribe_in(
cx.subscribe_in( &LanguageModelRegistry::global(cx),
&LanguageModelRegistry::global(cx), window,
window, |picker, _, event, window, cx| {
Self::handle_language_model_registry_event, match event {
), language_model::Event::ProviderStateChanged
subscription, | language_model::Event::AddedProvider(_)
], | language_model::Event::RemovedProvider(_) => {
let query = picker.query(cx);
picker.delegate.all_models = Arc::new(all_models(cx));
// Update matches will automatically drop the previous task
// if we get a provider event again
picker.update_matches(query, window, cx)
}
_ => {}
}
},
)],
} }
} }
fn handle_language_model_registry_event( fn get_active_model_index(
&mut self, entries: &[LanguageModelPickerEntry],
_registry: &Entity<LanguageModelRegistry>, active_model: Option<ConfiguredModel>,
event: &language_model::Event, ) -> usize {
window: &mut Window, entries
cx: &mut Context<Self>, .iter()
) { .position(|entry| {
match event { if let LanguageModelPickerEntry::Model(model) = entry {
language_model::Event::ProviderStateChanged active_model
| language_model::Event::AddedProvider(_) .as_ref()
| language_model::Event::RemovedProvider(_) => { .map(|active_model| {
self.picker.update(cx, |this, cx| { active_model.model.id() == model.model.id()
let query = this.query(cx); && active_model.provider.id() == model.model.provider_id()
this.delegate.all_models = Arc::new(Self::all_models(cx)); })
// Update matches will automatically drop the previous task .unwrap_or_default()
// if we get a provider event again } else {
this.update_matches(query, window, cx) false
}); }
} })
_ => {} .unwrap_or(0)
}
} }
/// Authenticates all providers in the [`LanguageModelRegistry`]. /// Authenticates all providers in the [`LanguageModelRegistry`].
@ -154,169 +222,9 @@ impl LanguageModelSelector {
}) })
} }
fn all_models(cx: &App) -> GroupedModels {
let mut recommended = Vec::new();
let mut recommended_set = HashSet::default();
for provider in LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
{
let models = provider.recommended_models(cx);
recommended_set.extend(models.iter().map(|model| (model.provider_id(), model.id())));
recommended.extend(
provider
.recommended_models(cx)
.into_iter()
.map(move |model| ModelInfo {
model: model.clone(),
icon: provider.icon(),
}),
);
}
let other_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| {
(
provider.id(),
provider
.provided_models(cx)
.into_iter()
.filter_map(|model| {
let not_included =
!recommended_set.contains(&(model.provider_id(), model.id()));
not_included.then(|| ModelInfo {
model: model.clone(),
icon: provider.icon(),
})
})
.collect::<Vec<_>>(),
)
})
.collect::<IndexMap<_, _>>();
GroupedModels {
recommended,
other: other_models,
}
}
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> { pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
(self.picker.read(cx).delegate.get_active_model)(cx) (self.get_active_model)(cx)
} }
fn get_active_model_index(
entries: &[LanguageModelPickerEntry],
active_model: Option<ConfiguredModel>,
) -> usize {
entries
.iter()
.position(|entry| {
if let LanguageModelPickerEntry::Model(model) = entry {
active_model
.as_ref()
.map(|active_model| {
active_model.model.id() == model.model.id()
&& active_model.provider.id() == model.model.provider_id()
})
.unwrap_or_default()
} else {
false
}
})
.unwrap_or(0)
}
}
impl EventEmitter<DismissEvent> for LanguageModelSelector {}
impl Focusable for LanguageModelSelector {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for LanguageModelSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(IntoElement)]
pub struct LanguageModelSelectorPopoverMenu<T, TT>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
language_model_selector: Entity<LanguageModelSelector>,
trigger: T,
tooltip: TT,
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
anchor: Corner,
}
impl<T, TT> LanguageModelSelectorPopoverMenu<T, TT>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
pub fn new(
language_model_selector: Entity<LanguageModelSelector>,
trigger: T,
tooltip: TT,
anchor: Corner,
) -> Self {
Self {
language_model_selector,
trigger,
tooltip,
handle: None,
anchor,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
self.handle = Some(handle);
self
}
}
impl<T, TT> RenderOnce for LanguageModelSelectorPopoverMenu<T, TT>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
{
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let language_model_selector = self.language_model_selector.clone();
PopoverMenu::new("model-switcher")
.menu(move |_window, _cx| Some(language_model_selector.clone()))
.trigger_with_tooltip(self.trigger, self.tooltip)
.anchor(self.anchor)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
}
}
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
}
pub struct LanguageModelPickerDelegate {
language_model_selector: WeakEntity<LanguageModelSelector>,
on_model_changed: OnModelChanged,
get_active_model: GetActiveModel,
all_models: Arc<GroupedModels>,
filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize,
} }
struct GroupedModels { struct GroupedModels {
@ -577,9 +485,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
} }
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) { fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.language_model_selector cx.emit(DismissEvent);
.update(cx, |_this, cx| cx.emit(DismissEvent))
.ok();
} }
fn render_match( fn render_match(

View file

@ -1,3 +1,7 @@
mod head;
pub mod highlighted_match_with_paths;
pub mod popover_menu;
use anyhow::Result; use anyhow::Result;
use editor::{ use editor::{
Editor, Editor,
@ -20,9 +24,6 @@ use ui::{
use util::ResultExt; use util::ResultExt;
use workspace::ModalView; use workspace::ModalView;
mod head;
pub mod highlighted_match_with_paths;
enum ElementContainer { enum ElementContainer {
List(ListState), List(ListState),
UniformList(UniformListScrollHandle), UniformList(UniformListScrollHandle),

View file

@ -0,0 +1,93 @@
use gpui::{
AnyView, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
};
use ui::{
App, ButtonCommon, FluentBuilder as _, IntoElement, PopoverMenu, PopoverMenuHandle,
PopoverTrigger, RenderOnce, Window, px,
};
use crate::{Picker, PickerDelegate};
pub struct PickerPopoverMenu<T, TT, P>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
P: PickerDelegate,
{
picker: Entity<Picker<P>>,
trigger: T,
tooltip: TT,
handle: Option<PopoverMenuHandle<Picker<P>>>,
anchor: Corner,
_subscriptions: Vec<Subscription>,
}
impl<T, TT, P> PickerPopoverMenu<T, TT, P>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
P: PickerDelegate,
{
pub fn new(
picker: Entity<Picker<P>>,
trigger: T,
tooltip: TT,
anchor: Corner,
cx: &mut App,
) -> Self {
Self {
_subscriptions: vec![cx.subscribe(&picker, |picker, &DismissEvent, cx| {
picker.update(cx, |_, cx| cx.emit(DismissEvent));
})],
picker,
trigger,
tooltip,
handle: None,
anchor,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<P>>) -> Self {
self.handle = Some(handle);
self
}
}
impl<T, TT, P> EventEmitter<DismissEvent> for PickerPopoverMenu<T, TT, P>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
P: PickerDelegate,
{
}
impl<T, TT, P> Focusable for PickerPopoverMenu<T, TT, P>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
P: PickerDelegate,
{
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl<T, TT, P> RenderOnce for PickerPopoverMenu<T, TT, P>
where
T: PopoverTrigger + ButtonCommon,
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
P: PickerDelegate,
{
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let picker = self.picker.clone();
PopoverMenu::new("popover-menu")
.menu(move |_window, _cx| Some(picker.clone()))
.trigger_with_tooltip(self.trigger, self.tooltip)
.anchor(self.anchor)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
}
}