1use assistant_slash_command::SlashCommandRegistry;
2use gpui::DismissEvent;
3use gpui::WeakView;
4use picker::PickerEditorPosition;
5
6use std::sync::Arc;
7use ui::ListItemSpacing;
8
9use gpui::SharedString;
10use gpui::Task;
11use picker::{Picker, PickerDelegate};
12use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
13
14use crate::assistant_panel::ContextEditor;
15
16#[derive(IntoElement)]
17pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
18 registry: Arc<SlashCommandRegistry>,
19 active_context_editor: WeakView<ContextEditor>,
20 trigger: T,
21}
22
23#[derive(Clone)]
24struct SlashCommandInfo {
25 name: SharedString,
26 description: SharedString,
27}
28
29pub(crate) struct SlashCommandDelegate {
30 all_commands: Vec<SlashCommandInfo>,
31 filtered_commands: Vec<SlashCommandInfo>,
32 active_context_editor: WeakView<ContextEditor>,
33 selected_index: usize,
34}
35
36impl<T: PopoverTrigger> SlashCommandSelector<T> {
37 pub(crate) fn new(
38 registry: Arc<SlashCommandRegistry>,
39 active_context_editor: WeakView<ContextEditor>,
40 trigger: T,
41 ) -> Self {
42 SlashCommandSelector {
43 registry,
44 active_context_editor,
45 trigger,
46 }
47 }
48}
49
50impl PickerDelegate for SlashCommandDelegate {
51 type ListItem = ListItem;
52
53 fn match_count(&self) -> usize {
54 self.filtered_commands.len()
55 }
56
57 fn selected_index(&self) -> usize {
58 self.selected_index
59 }
60
61 fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
62 self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
63 cx.notify();
64 }
65
66 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
67 "Select a command...".into()
68 }
69
70 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
71 let all_commands = self.all_commands.clone();
72 cx.spawn(|this, mut cx| async move {
73 let filtered_commands = cx
74 .background_executor()
75 .spawn(async move {
76 if query.is_empty() {
77 all_commands
78 } else {
79 all_commands
80 .into_iter()
81 .filter(|model_info| {
82 model_info
83 .name
84 .to_lowercase()
85 .contains(&query.to_lowercase())
86 })
87 .collect()
88 }
89 })
90 .await;
91
92 this.update(&mut cx, |this, cx| {
93 this.delegate.filtered_commands = filtered_commands;
94 this.delegate.set_selected_index(0, cx);
95 cx.notify();
96 })
97 .ok();
98 })
99 }
100
101 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
102 if let Some(command) = self.filtered_commands.get(self.selected_index) {
103 self.active_context_editor
104 .update(cx, |context_editor, cx| {
105 context_editor.insert_command(&command.name, cx)
106 })
107 .ok();
108 cx.emit(DismissEvent);
109 }
110 }
111
112 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
113
114 fn editor_position(&self) -> PickerEditorPosition {
115 PickerEditorPosition::End
116 }
117
118 fn render_match(
119 &self,
120 ix: usize,
121 selected: bool,
122 _: &mut ViewContext<Picker<Self>>,
123 ) -> Option<Self::ListItem> {
124 let command_info = self.filtered_commands.get(ix)?;
125
126 Some(
127 ListItem::new(ix)
128 .inset(true)
129 .spacing(ListItemSpacing::Sparse)
130 .selected(selected)
131 .child(
132 h_flex().w_full().min_w(px(220.)).child(
133 v_flex()
134 .child(
135 Label::new(format!("/{}", command_info.name))
136 .size(LabelSize::Small),
137 )
138 .child(
139 Label::new(command_info.description.clone())
140 .size(LabelSize::Small)
141 .color(Color::Muted),
142 ),
143 ),
144 ),
145 )
146 }
147}
148
149impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
150 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
151 let all_models = self
152 .registry
153 .featured_command_names()
154 .into_iter()
155 .filter_map(|command_name| {
156 let command = self.registry.command(&command_name)?;
157 let menu_text = SharedString::from(Arc::from(command.menu_text()));
158 Some(SlashCommandInfo {
159 name: command_name.into(),
160 description: menu_text,
161 })
162 })
163 .collect::<Vec<_>>();
164
165 let delegate = SlashCommandDelegate {
166 all_commands: all_models.clone(),
167 active_context_editor: self.active_context_editor.clone(),
168 filtered_commands: all_models,
169 selected_index: 0,
170 };
171
172 let picker_view = cx.new_view(|cx| {
173 let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
174 picker
175 });
176
177 let handle = self
178 .active_context_editor
179 .update(cx, |this, _| this.slash_menu_handle.clone())
180 .ok();
181 PopoverMenu::new("model-switcher")
182 .menu(move |_cx| Some(picker_view.clone()))
183 .trigger(self.trigger)
184 .attach(gpui::AnchorCorner::TopLeft)
185 .anchor(gpui::AnchorCorner::BottomLeft)
186 .offset(gpui::Point {
187 x: px(0.0),
188 y: px(-16.0),
189 })
190 .when_some(handle, |this, handle| this.with_handle(handle))
191 }
192}