slash_command_picker.rs

  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}