assistant: Add the "create your command" item (#16467)

This PR adds an extra item to the slash command picker that links users to the doc that teaches how to create a custom one.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
This commit is contained in:
Piotr Osiewicz 2024-08-19 17:29:16 +02:00 committed by GitHub
parent 0bea4d5fa6
commit bac8e81e73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 144 additions and 30 deletions

View File

@ -1,9 +1,11 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent; use gpui::DismissEvent;
use gpui::WeakView; use gpui::WeakView;
use picker::PickerEditorPosition; use picker::PickerEditorPosition;
use std::sync::Arc;
use ui::ListItemSpacing; use ui::ListItemSpacing;
use gpui::SharedString; use gpui::SharedString;
@ -24,11 +26,31 @@ pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
struct SlashCommandInfo { struct SlashCommandInfo {
name: SharedString, name: SharedString,
description: SharedString, description: SharedString,
args: Option<SharedString>,
}
#[derive(Clone)]
enum SlashCommandEntry {
Info(SlashCommandInfo),
Advert {
name: SharedString,
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
}
impl AsRef<str> for SlashCommandEntry {
fn as_ref(&self) -> &str {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
}
}
} }
pub(crate) struct SlashCommandDelegate { pub(crate) struct SlashCommandDelegate {
all_commands: Vec<SlashCommandInfo>, all_commands: Vec<SlashCommandEntry>,
filtered_commands: Vec<SlashCommandInfo>, filtered_commands: Vec<SlashCommandEntry>,
active_context_editor: WeakView<ContextEditor>, active_context_editor: WeakView<ContextEditor>,
selected_index: usize, selected_index: usize,
} }
@ -80,7 +102,7 @@ impl PickerDelegate for SlashCommandDelegate {
.into_iter() .into_iter()
.filter(|model_info| { .filter(|model_info| {
model_info model_info
.name .as_ref()
.to_lowercase() .to_lowercase()
.contains(&query.to_lowercase()) .contains(&query.to_lowercase())
}) })
@ -98,13 +120,42 @@ impl PickerDelegate for SlashCommandDelegate {
}) })
} }
fn separators_after_indices(&self) -> Vec<usize> {
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<Picker<Self>>) { fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(command) = self.filtered_commands.get(self.selected_index) { if let Some(command) = self.filtered_commands.get(self.selected_index) {
self.active_context_editor if let SlashCommandEntry::Info(info) = command {
.update(cx, |context_editor, cx| { self.active_context_editor
context_editor.insert_command(&command.name, cx) .update(cx, |context_editor, cx| {
}) context_editor.insert_command(&info.name, cx)
.ok(); })
.ok();
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
on_confirm(cx);
}
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
} }
@ -119,30 +170,63 @@ impl PickerDelegate for SlashCommandDelegate {
&self, &self,
ix: usize, ix: usize,
selected: bool, selected: bool,
_: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let command_info = self.filtered_commands.get(ix)?; let command_info = self.filtered_commands.get(ix)?;
Some( match command_info {
ListItem::new(ix) SlashCommandEntry::Info(info) => Some(
.inset(true) ListItem::new(ix)
.spacing(ListItemSpacing::Sparse) .inset(true)
.selected(selected) .spacing(ListItemSpacing::Sparse)
.child( .selected(selected)
h_flex().w_full().min_w(px(220.)).child( .child(
v_flex() h_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.min_w(px(220.))
.child( .child(
Label::new(format!("/{}", command_info.name)) v_flex()
.size(LabelSize::Small), .child(
) h_flex()
.child( .child(div().font_buffer(cx).child({
Label::new(command_info.description.clone()) let mut label = format!("/{}", info.name);
.size(LabelSize::Small) if let Some(args) =
.color(Color::Muted), 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<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
.filter_map(|command_name| { .filter_map(|command_name| {
let command = self.registry.command(&command_name)?; let command = self.registry.command(&command_name)?;
let menu_text = SharedString::from(Arc::from(command.menu_text())); 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(), name: command_name.into(),
description: menu_text, 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::<Vec<_>>(); .collect::<Vec<_>>();
let delegate = SlashCommandDelegate { let delegate = SlashCommandDelegate {

View File

@ -524,7 +524,7 @@ impl<D: PickerDelegate> Picker<D> {
picker picker
.border_color(cx.theme().colors().border_variant) .border_color(cx.theme().colors().border_variant)
.border_b_1() .border_b_1()
.pb(px(-1.0)) .py(px(-1.0))
}, },
) )
} }