Adjust design of the slash command picker (#19973)

This PR removes the quote selection icon button from the footer and adds
it in the picker, and adds an icon field to each command entry. Final
result looks like:


https://github.com/user-attachments/assets/d177f1c1-b6f6-4652-9434-f6291b279e34

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2024-10-30 19:42:42 -03:00 committed by GitHub
parent f80eb264fb
commit 6d5784daa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 185 additions and 110 deletions

1
assets/icons/wand.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>

After

Width:  |  Height:  |  Size: 414 B

View file

@ -73,12 +73,11 @@ use std::{
}; };
use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::SelectionGoal; use text::SelectionGoal;
use ui::TintColor;
use ui::{ use ui::{
prelude::*, prelude::*,
utils::{format_distance_from_now, DateTimeType}, utils::{format_distance_from_now, DateTimeType},
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem, Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip, ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
}; };
use util::{maybe, ResultExt}; use util::{maybe, ResultExt};
use workspace::{ use workspace::{
@ -4006,13 +4005,7 @@ impl Render for ContextEditor {
} else { } else {
None None
}; };
let focus_handle = self
.workspace
.update(cx, |workspace, cx| {
Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
})
.ok()
.flatten();
v_flex() v_flex()
.key_context("ContextEditor") .key_context("ContextEditor")
.capture_action(cx.listener(ContextEditor::cancel)) .capture_action(cx.listener(ContextEditor::cancel))
@ -4060,28 +4053,7 @@ impl Render for ContextEditor {
.child( .child(
h_flex() h_flex()
.gap_1() .gap_1()
.child(render_inject_context_menu(cx.view().downgrade(), cx)) .child(render_inject_context_menu(cx.view().downgrade(), cx)),
.child(
IconButton::new("quote-button", IconName::Quote)
.icon_size(IconSize::Small)
.on_click(|_, cx| {
cx.dispatch_action(QuoteSelection.boxed_clone());
})
.tooltip(move |cx| {
cx.new_view(|cx| {
Tooltip::new("Insert Selection").key_binding(
focus_handle.as_ref().and_then(|handle| {
KeyBinding::for_action_in(
&QuoteSelection,
&handle,
cx,
)
}),
)
})
.into()
}),
),
) )
.child( .child(
h_flex() h_flex()
@ -4376,6 +4348,7 @@ fn render_inject_context_menu(
Button::new("trigger", "Add Context") Button::new("trigger", "Add Context")
.icon(IconName::Plus) .icon(IconName::Plus)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)), .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
) )
@ -4550,7 +4523,7 @@ impl Render for ContextEditorToolbarItem {
.w_full() .w_full()
.justify_between() .justify_between()
.gap_2() .gap_2()
.child(Label::new("Insert Context")) .child(Label::new("Add Context"))
.child(Label::new("/ command").color(Color::Muted)) .child(Label::new("/ command").color(Color::Muted))
.into_any() .into_any()
}, },
@ -4574,7 +4547,7 @@ impl Render for ContextEditorToolbarItem {
} }
}, },
) )
.action("Insert Selection", QuoteSelection.boxed_clone()) .action("Add Selection", QuoteSelection.boxed_clone())
})) }))
} }
}), }),

View file

@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb}; use semantic_index::{FileSummary, SemanticDb};
use smol::channel; use smol::channel;
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{atomic::AtomicBool, Arc};
use ui::{BorrowAppContext, WindowContext}; use ui::{prelude::*, BorrowAppContext, WindowContext};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand {
"Automatically infer what context to add".into() "Automatically infer what context to add".into()
} }
fn icon(&self) -> IconName {
IconName::Wand
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate}; use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc}; use std::sync::{atomic::AtomicBool, Arc};
use text::OffsetRangeExt; use text::OffsetRangeExt;
use ui::prelude::*;
use workspace::Workspace; use workspace::Workspace;
pub(crate) struct DeltaSlashCommand; pub(crate) struct DeltaSlashCommand;
@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand {
self.description() self.description()
} }
fn icon(&self) -> IconName {
IconName::Diff
}
fn requires_argument(&self) -> bool { fn requires_argument(&self) -> bool {
false false
} }

View file

@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
"Insert diagnostics".into() "Insert diagnostics".into()
} }
fn icon(&self) -> IconName {
IconName::XCircle
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand {
} }
fn description(&self) -> String { fn description(&self) -> String {
"Insert file".into() "Insert file and/or directory".into()
} }
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand {
true true
} }
fn icon(&self) -> IconName {
IconName::File
}
fn complete_argument( fn complete_argument(
self: Arc<Self>, self: Arc<Self>,
arguments: &[String], arguments: &[String],

View file

@ -24,7 +24,8 @@ use std::{
ops::DerefMut, ops::DerefMut,
sync::{atomic::AtomicBool, Arc}, sync::{atomic::AtomicBool, Arc},
}; };
use ui::{BorrowAppContext as _, IconName};
use ui::prelude::*;
use workspace::Workspace; use workspace::Workspace;
pub struct ProjectSlashCommand { pub struct ProjectSlashCommand {
@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand {
"Generate a semantic search based on context".into() "Generate a semantic search based on context".into()
} }
fn icon(&self) -> IconName {
IconName::Folder
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand {
"Insert prompt from library".into() "Insert prompt from library".into()
} }
fn icon(&self) -> IconName {
IconName::Library
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand {
"Search your project semantically".into() "Search your project semantically".into()
} }
fn icon(&self) -> IconName {
IconName::SearchCode
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand {
"Insert symbols for active tab".into() "Insert symbols for active tab".into()
} }
fn icon(&self) -> IconName {
IconName::ListTree
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -12,7 +12,7 @@ use std::{
path::PathBuf, path::PathBuf,
sync::{atomic::AtomicBool, Arc}, sync::{atomic::AtomicBool, Arc},
}; };
use ui::{ActiveTheme, WindowContext}; use ui::{prelude::*, ActiveTheme, WindowContext};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand {
"Insert open tabs (active tab by default)".to_owned() "Insert open tabs (active tab by default)".to_owned()
} }
fn icon(&self) -> IconName {
IconName::FileTree
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand {
"Insert terminal output".into() "Insert terminal output".into()
} }
fn icon(&self) -> IconName {
IconName::Terminal
}
fn menu_text(&self) -> String { fn menu_text(&self) -> String {
self.description() self.description()
} }

View file

@ -1,19 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry; use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent;
use gpui::WeakView;
use picker::PickerEditorPosition;
use ui::ListItemSpacing; use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use gpui::SharedString; use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use gpui::Task;
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor; use crate::assistant_panel::ContextEditor;
use crate::QuoteSelection;
#[derive(IntoElement)] #[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> { pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
@ -27,6 +21,7 @@ struct SlashCommandInfo {
name: SharedString, name: SharedString,
description: SharedString, description: SharedString,
args: Option<SharedString>, args: Option<SharedString>,
icon: IconName,
} }
#[derive(Clone)] #[derive(Clone)]
@ -37,6 +32,7 @@ enum SlashCommandEntry {
renderer: fn(&mut WindowContext<'_>) -> AnyElement, renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>), on_confirm: fn(&mut WindowContext<'_>),
}, },
QuoteButton,
} }
impl AsRef<str> for SlashCommandEntry { impl AsRef<str> for SlashCommandEntry {
@ -44,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
match self { match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. }) SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name, | SlashCommandEntry::Advert { name, .. } => name,
SlashCommandEntry::QuoteButton => "Quote Selection",
} }
} }
} }
@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate {
} }
ret 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) {
if let SlashCommandEntry::Info(info) = command { match command {
self.active_context_editor SlashCommandEntry::Info(info) => {
.update(cx, |context_editor, cx| { self.active_context_editor
context_editor.insert_command(&info.name, cx) .update(cx, |context_editor, cx| {
}) context_editor.insert_command(&info.name, cx)
.ok(); })
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command { .ok();
on_confirm(cx); }
SlashCommandEntry::QuoteButton => {
cx.dispatch_action(Box::new(QuoteSelection));
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
}
} }
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate {
.spacing(ListItemSpacing::Dense) .spacing(ListItemSpacing::Dense)
.selected(selected) .selected(selected)
.child( .child(
h_flex() v_flex()
.group(format!("command-entry-label-{ix}")) .group(format!("command-entry-label-{ix}"))
.w_full() .w_full()
.min_w(px(250.)) .min_w(px(250.))
.child( .child(
v_flex() h_flex()
.child( .gap_1p5()
h_flex() .child(Icon::new(info.icon).size(IconSize::XSmall))
.child(div().font_buffer(cx).child({ .child(div().font_buffer(cx).child({
let mut label = format!("/{}", info.name); let mut label = format!("{}", info.name);
if let Some(args) = if let Some(args) = info.args.as_ref().filter(|_| selected)
info.args.as_ref().filter(|_| selected) {
{ label.push_str(&args);
label.push_str(&args); }
} Label::new(label).size(LabelSize::Small)
Label::new(label).size(LabelSize::Small) }))
})) .children(info.args.clone().filter(|_| !selected).map(
.children(info.args.clone().filter(|_| !selected).map( |args| {
|args| { div()
div() .font_buffer(cx)
.font_buffer(cx) .child(
.child( Label::new(args)
Label::new(args) .size(LabelSize::Small)
.size(LabelSize::Small) .color(Color::Muted),
.color(Color::Muted), )
) .visible_on_hover(format!(
.visible_on_hover(format!( "command-entry-label-{ix}"
"command-entry-label-{ix}" ))
)) },
}, )),
)), )
) .child(
.child( Label::new(info.description.clone())
Label::new(info.description.clone()) .size(LabelSize::Small)
.size(LabelSize::Small) .color(Color::Muted),
.color(Color::Muted),
),
), ),
), ),
), ),
SlashCommandEntry::QuoteButton => {
let focus = cx.focus_handle();
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(
v_flex()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("selection").size(LabelSize::Small),
),
),
)
.child(
h_flex()
.gap_1p5()
.child(
Label::new("Insert editor selection")
.color(Color::Muted)
.size(LabelSize::Small),
)
.children(key_binding.map(|kb| kb.render(cx))),
),
),
)
}
SlashCommandEntry::Advert { renderer, .. } => Some( SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix) ListItem::new(ix)
.inset(true) .inset(true)
@ -251,31 +287,50 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
name: command_name.into(), name: command_name.into(),
description: menu_text, description: menu_text,
args, args,
icon: command.icon(),
})) }))
}) })
.chain([SlashCommandEntry::Advert { .chain([
name: "create-your-command".into(), SlashCommandEntry::Advert {
renderer: |cx| { name: "create-your-command".into(),
v_flex() renderer: |cx| {
.child( v_flex()
h_flex() .w_full()
.font_buffer(cx) .child(
.items_center() h_flex()
.gap_1() .w_full()
.child(div().font_buffer(cx).child( .font_buffer(cx)
Label::new("create-your-command").size(LabelSize::Small), .items_center()
)) .justify_between()
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)), .child(
) h_flex()
.child( .items_center()
Label::new("Learn how to create a custom command") .gap_1p5()
.size(LabelSize::Small) .child(Icon::new(IconName::Plus).size(IconSize::XSmall))
.color(Color::Muted), .child(
) div().font_buffer(cx).child(
.into_any_element() Label::new("create-your-command")
.size(LabelSize::Small),
),
),
)
.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"),
}, },
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"), SlashCommandEntry::QuoteButton,
}]) ])
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let delegate = SlashCommandDelegate { let delegate = SlashCommandDelegate {

View file

@ -62,6 +62,9 @@ pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent
pub trait SlashCommand: 'static + Send + Sync { pub trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String; fn name(&self) -> String;
fn icon(&self) -> IconName {
IconName::Slash
}
fn label(&self, _cx: &AppContext) -> CodeLabel { fn label(&self, _cx: &AppContext) -> CodeLabel {
CodeLabel::plain(self.name(), None) CodeLabel::plain(self.name(), None)
} }

View file

@ -284,6 +284,7 @@ pub enum IconName {
Update, Update,
UserGroup, UserGroup,
Visible, Visible,
Wand,
Warning, Warning,
WholeWord, WholeWord,
XCircle, XCircle,