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, 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}