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