
- Fix component preview widths for git panel - Fix buttons getting pushed off the screen in git panel Release Notes: - N/A --------- Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
354 lines
13 KiB
Rust
354 lines
13 KiB
Rust
use std::sync::Arc;
|
|
|
|
use assistant_slash_command::SlashCommandWorkingSet;
|
|
use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
|
|
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
|
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
|
|
|
use crate::context_editor::ContextEditor;
|
|
|
|
#[derive(IntoElement)]
|
|
pub(super) struct SlashCommandSelector<T, TT>
|
|
where
|
|
T: PopoverTrigger + ButtonCommon,
|
|
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
|
{
|
|
working_set: Arc<SlashCommandWorkingSet>,
|
|
active_context_editor: WeakEntity<ContextEditor>,
|
|
trigger: T,
|
|
tooltip: TT,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct SlashCommandInfo {
|
|
name: SharedString,
|
|
description: SharedString,
|
|
args: Option<SharedString>,
|
|
icon: IconName,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum SlashCommandEntry {
|
|
Info(SlashCommandInfo),
|
|
Advert {
|
|
name: SharedString,
|
|
renderer: fn(&mut Window, &mut App) -> AnyElement,
|
|
on_confirm: fn(&mut Window, &mut App),
|
|
},
|
|
}
|
|
|
|
impl AsRef<str> for SlashCommandEntry {
|
|
fn as_ref(&self) -> &str {
|
|
match self {
|
|
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
|
|
| SlashCommandEntry::Advert { name, .. } => name,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct SlashCommandDelegate {
|
|
all_commands: Vec<SlashCommandEntry>,
|
|
filtered_commands: Vec<SlashCommandEntry>,
|
|
active_context_editor: WeakEntity<ContextEditor>,
|
|
selected_index: usize,
|
|
}
|
|
|
|
impl<T, TT> SlashCommandSelector<T, TT>
|
|
where
|
|
T: PopoverTrigger + ButtonCommon,
|
|
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
|
{
|
|
pub(crate) fn new(
|
|
working_set: Arc<SlashCommandWorkingSet>,
|
|
active_context_editor: WeakEntity<ContextEditor>,
|
|
trigger: T,
|
|
tooltip: TT,
|
|
) -> Self {
|
|
SlashCommandSelector {
|
|
working_set,
|
|
active_context_editor,
|
|
trigger,
|
|
tooltip,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PickerDelegate for SlashCommandDelegate {
|
|
type ListItem = ListItem;
|
|
|
|
fn match_count(&self) -> usize {
|
|
self.filtered_commands.len()
|
|
}
|
|
|
|
fn selected_index(&self) -> usize {
|
|
self.selected_index
|
|
}
|
|
|
|
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
|
|
cx.notify();
|
|
}
|
|
|
|
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
|
"Select a command...".into()
|
|
}
|
|
|
|
fn update_matches(
|
|
&mut self,
|
|
query: String,
|
|
window: &mut Window,
|
|
cx: &mut Context<Picker<Self>>,
|
|
) -> Task<()> {
|
|
let all_commands = self.all_commands.clone();
|
|
cx.spawn_in(window, |this, mut cx| async move {
|
|
let filtered_commands = cx
|
|
.background_spawn(async move {
|
|
if query.is_empty() {
|
|
all_commands
|
|
} else {
|
|
all_commands
|
|
.into_iter()
|
|
.filter(|model_info| {
|
|
model_info
|
|
.as_ref()
|
|
.to_lowercase()
|
|
.contains(&query.to_lowercase())
|
|
})
|
|
.collect()
|
|
}
|
|
})
|
|
.await;
|
|
|
|
this.update_in(&mut cx, |this, window, cx| {
|
|
this.delegate.filtered_commands = filtered_commands;
|
|
this.delegate.set_selected_index(0, window, cx);
|
|
cx.notify();
|
|
})
|
|
.ok();
|
|
})
|
|
}
|
|
|
|
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, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
|
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
|
match command {
|
|
SlashCommandEntry::Info(info) => {
|
|
self.active_context_editor
|
|
.update(cx, |context_editor, cx| {
|
|
context_editor.insert_command(&info.name, window, cx)
|
|
})
|
|
.ok();
|
|
}
|
|
SlashCommandEntry::Advert { on_confirm, .. } => {
|
|
on_confirm(window, cx);
|
|
}
|
|
}
|
|
cx.emit(DismissEvent);
|
|
}
|
|
}
|
|
|
|
fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
|
|
|
|
fn editor_position(&self) -> PickerEditorPosition {
|
|
PickerEditorPosition::End
|
|
}
|
|
|
|
fn render_match(
|
|
&self,
|
|
ix: usize,
|
|
selected: bool,
|
|
window: &mut Window,
|
|
cx: &mut Context<Picker<Self>>,
|
|
) -> Option<Self::ListItem> {
|
|
let command_info = self.filtered_commands.get(ix)?;
|
|
|
|
match command_info {
|
|
SlashCommandEntry::Info(info) => Some(
|
|
ListItem::new(ix)
|
|
.inset(true)
|
|
.spacing(ListItemSpacing::Dense)
|
|
.toggle_state(selected)
|
|
.tooltip({
|
|
let description = info.description.clone();
|
|
move |_, cx| cx.new(|_| Tooltip::new(description.clone())).into()
|
|
})
|
|
.child(
|
|
v_flex()
|
|
.group(format!("command-entry-label-{ix}"))
|
|
.w_full()
|
|
.py_0p5()
|
|
.min_w(px(250.))
|
|
.max_w(px(400.))
|
|
.child(
|
|
h_flex()
|
|
.gap_1p5()
|
|
.child(
|
|
Icon::new(info.icon)
|
|
.size(IconSize::XSmall)
|
|
.color(Color::Muted),
|
|
)
|
|
.child({
|
|
let mut label = format!("{}", info.name);
|
|
if let Some(args) = info.args.as_ref().filter(|_| selected)
|
|
{
|
|
label.push_str(&args);
|
|
}
|
|
Label::new(label)
|
|
.single_line()
|
|
.size(LabelSize::Small)
|
|
.buffer_font(cx)
|
|
})
|
|
.children(info.args.clone().filter(|_| !selected).map(
|
|
|args| {
|
|
div()
|
|
.child(
|
|
Label::new(args)
|
|
.single_line()
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted)
|
|
.buffer_font(cx),
|
|
)
|
|
.visible_on_hover(format!(
|
|
"command-entry-label-{ix}"
|
|
))
|
|
},
|
|
)),
|
|
)
|
|
.child(
|
|
Label::new(info.description.clone())
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted)
|
|
.truncate(),
|
|
),
|
|
),
|
|
),
|
|
SlashCommandEntry::Advert { renderer, .. } => Some(
|
|
ListItem::new(ix)
|
|
.inset(true)
|
|
.spacing(ListItemSpacing::Dense)
|
|
.toggle_state(selected)
|
|
.child(renderer(window, cx)),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T, TT> RenderOnce for SlashCommandSelector<T, TT>
|
|
where
|
|
T: PopoverTrigger + ButtonCommon,
|
|
TT: Fn(&mut Window, &mut App) -> AnyView + 'static,
|
|
{
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let all_models = self
|
|
.working_set
|
|
.featured_command_names(cx)
|
|
.into_iter()
|
|
.filter_map(|command_name| {
|
|
let command = self.working_set.command(&command_name, cx)?;
|
|
let menu_text = SharedString::from(Arc::from(command.menu_text()));
|
|
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,
|
|
icon: command.icon(),
|
|
}))
|
|
})
|
|
.chain([SlashCommandEntry::Advert {
|
|
name: "create-your-command".into(),
|
|
renderer: |_, cx| {
|
|
v_flex()
|
|
.w_full()
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.font_buffer(cx)
|
|
.items_center()
|
|
.justify_between()
|
|
.child(
|
|
h_flex()
|
|
.items_center()
|
|
.gap_1p5()
|
|
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
|
|
.child(
|
|
Label::new("create-your-command")
|
|
.size(LabelSize::Small)
|
|
.buffer_font(cx),
|
|
),
|
|
)
|
|
.child(
|
|
Icon::new(IconName::ArrowUpRight)
|
|
.size(IconSize::XSmall)
|
|
.color(Color::Muted),
|
|
),
|
|
)
|
|
.child(
|
|
Label::new("Create your 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<_>>();
|
|
|
|
let delegate = SlashCommandDelegate {
|
|
all_commands: all_models.clone(),
|
|
active_context_editor: self.active_context_editor.clone(),
|
|
filtered_commands: all_models,
|
|
selected_index: 0,
|
|
};
|
|
|
|
let picker_view = cx.new(|cx| {
|
|
let picker =
|
|
Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()));
|
|
picker
|
|
});
|
|
|
|
let handle = self
|
|
.active_context_editor
|
|
.update(cx, |this, _| this.slash_menu_handle.clone())
|
|
.ok();
|
|
PopoverMenu::new("model-switcher")
|
|
.menu(move |_window, _cx| Some(picker_view.clone()))
|
|
.trigger_with_tooltip(self.trigger, self.tooltip)
|
|
.attach(gpui::Corner::TopLeft)
|
|
.anchor(gpui::Corner::BottomLeft)
|
|
.offset(gpui::Point {
|
|
x: px(0.0),
|
|
y: px(-2.0),
|
|
})
|
|
.when_some(handle, |this, handle| this.with_handle(handle))
|
|
}
|
|
}
|