diff --git a/crates/assistant/src/slash_command_picker.rs b/crates/assistant/src/slash_command_picker.rs index fb2a0f0a1e..4b57dcfb33 100644 --- a/crates/assistant/src/slash_command_picker.rs +++ b/crates/assistant/src/slash_command_picker.rs @@ -1,9 +1,11 @@ +use std::sync::Arc; + use assistant_slash_command::SlashCommandRegistry; +use gpui::AnyElement; use gpui::DismissEvent; use gpui::WeakView; use picker::PickerEditorPosition; -use std::sync::Arc; use ui::ListItemSpacing; use gpui::SharedString; @@ -24,11 +26,31 @@ pub(super) struct SlashCommandSelector { struct SlashCommandInfo { name: SharedString, description: SharedString, + args: Option, +} + +#[derive(Clone)] +enum SlashCommandEntry { + Info(SlashCommandInfo), + Advert { + name: SharedString, + renderer: fn(&mut WindowContext<'_>) -> AnyElement, + on_confirm: fn(&mut WindowContext<'_>), + }, +} + +impl AsRef for SlashCommandEntry { + fn as_ref(&self) -> &str { + match self { + SlashCommandEntry::Info(SlashCommandInfo { name, .. }) + | SlashCommandEntry::Advert { name, .. } => name, + } + } } pub(crate) struct SlashCommandDelegate { - all_commands: Vec, - filtered_commands: Vec, + all_commands: Vec, + filtered_commands: Vec, active_context_editor: WeakView, selected_index: usize, } @@ -80,7 +102,7 @@ impl PickerDelegate for SlashCommandDelegate { .into_iter() .filter(|model_info| { model_info - .name + .as_ref() .to_lowercase() .contains(&query.to_lowercase()) }) @@ -98,13 +120,42 @@ impl PickerDelegate for SlashCommandDelegate { }) } + fn separators_after_indices(&self) -> Vec { + let mut ret = vec![]; + let mut previous_is_advert = false; + + for (index, command) in self.filtered_commands.iter().enumerate() { + if previous_is_advert { + if let SlashCommandEntry::Info(_) = command { + previous_is_advert = false; + debug_assert_ne!( + index, 0, + "index cannot be zero, as we can never have a separator at 0th position" + ); + ret.push(index - 1); + } + } else { + if let SlashCommandEntry::Advert { .. } = command { + previous_is_advert = true; + if index != 0 { + ret.push(index - 1); + } + } + } + } + ret + } fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { if let Some(command) = self.filtered_commands.get(self.selected_index) { - self.active_context_editor - .update(cx, |context_editor, cx| { - context_editor.insert_command(&command.name, cx) - }) - .ok(); + if let SlashCommandEntry::Info(info) = command { + self.active_context_editor + .update(cx, |context_editor, cx| { + context_editor.insert_command(&info.name, cx) + }) + .ok(); + } else if let SlashCommandEntry::Advert { on_confirm, .. } = command { + on_confirm(cx); + } cx.emit(DismissEvent); } } @@ -119,30 +170,63 @@ impl PickerDelegate for SlashCommandDelegate { &self, ix: usize, selected: bool, - _: &mut ViewContext>, + cx: &mut ViewContext>, ) -> Option { let command_info = self.filtered_commands.get(ix)?; - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child( - h_flex().w_full().min_w(px(220.)).child( - v_flex() + match command_info { + SlashCommandEntry::Info(info) => Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child( + h_flex() + .group(format!("command-entry-label-{ix}")) + .w_full() + .min_w(px(220.)) .child( - Label::new(format!("/{}", command_info.name)) - .size(LabelSize::Small), - ) - .child( - Label::new(command_info.description.clone()) - .size(LabelSize::Small) - .color(Color::Muted), + v_flex() + .child( + h_flex() + .child(div().font_buffer(cx).child({ + let mut label = format!("/{}", info.name); + if let Some(args) = + info.args.as_ref().filter(|_| selected) + { + label.push_str(&args); + } + Label::new(label).size(LabelSize::Small) + })) + .children(info.args.clone().filter(|_| !selected).map( + |args| { + div() + .font_buffer(cx) + .child( + Label::new(args).size(LabelSize::Small), + ) + .visible_on_hover(format!( + "command-entry-label-{ix}" + )) + }, + )), + ) + .child( + Label::new(info.description.clone()) + .size(LabelSize::Small) + .color(Color::Muted), + ), ), ), - ), - ) + ), + SlashCommandEntry::Advert { renderer, .. } => Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child(renderer(cx)), + ), + } } } @@ -155,11 +239,41 @@ impl RenderOnce for SlashCommandSelector { .filter_map(|command_name| { let command = self.registry.command(&command_name)?; let menu_text = SharedString::from(Arc::from(command.menu_text())); - Some(SlashCommandInfo { + let label = command.label(cx); + let args = label.filter_range.end.ne(&label.text.len()).then(|| { + SharedString::from( + label.text[label.filter_range.end..label.text.len()].to_owned(), + ) + }); + Some(SlashCommandEntry::Info(SlashCommandInfo { name: command_name.into(), description: menu_text, - }) + args, + })) }) + .chain([SlashCommandEntry::Advert { + name: "create-your-command".into(), + renderer: |cx| { + v_flex() + .child( + h_flex() + .font_buffer(cx) + .items_center() + .gap_1() + .child(div().font_buffer(cx).child( + Label::new("create-your-command").size(LabelSize::Small), + )) + .child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)), + ) + .child( + Label::new("Learn how to create a custom command") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), + }]) .collect::>(); let delegate = SlashCommandDelegate { diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 376810e417..dc661d91fb 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -524,7 +524,7 @@ impl Picker { picker .border_color(cx.theme().colors().border_variant) .border_b_1() - .pb(px(-1.0)) + .py(px(-1.0)) }, ) }