assistant: Add action footer and refine slash command popover (#16360)

Danilo Leal , Piotr Osiewicz , Nate Butler , and Kirill Bulatov created

- [x] Put the slash command popover on the footer
- [x] Refine the popover (change it to a picker)
- [x] Add more options dropdown on the assistant's toolbar
- [x] Add quote selection button on the footer

---

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>

Change summary

assets/icons/ellipsis_vertical.svg           |   1 
assets/icons/slash.svg                       |   1 
assets/icons/slash_square.svg                |   1 
crates/assistant/src/assistant.rs            |   1 
crates/assistant/src/assistant_panel.rs      | 234 ++++++++++++++-------
crates/assistant/src/slash_command_picker.rs | 201 ++++++++++++++++++
crates/picker/src/picker.rs                  |  46 +++
crates/ui/src/components/icon.rs             |   6 
8 files changed, 402 insertions(+), 89 deletions(-)

Detailed changes

assets/icons/ellipsis_vertical.svg 🔗

@@ -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-ellipsis-vertical"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>

assets/icons/slash.svg 🔗

@@ -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-slash"><path d="M22 2 2 22"/></svg>

assets/icons/slash_square.svg 🔗

@@ -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-square-slash"><rect width="18" height="18" x="3" y="3" rx="2"/><line x1="9" x2="15" y1="15" y2="9"/></svg>

crates/assistant/src/assistant.rs 🔗

@@ -9,6 +9,7 @@ mod model_selector;
 mod prompt_library;
 mod prompts;
 mod slash_command;
+mod slash_command_picker;
 pub mod slash_command_settings;
 mod streaming_diff;
 mod terminal_inline_assistant;

crates/assistant/src/assistant_panel.rs 🔗

@@ -9,6 +9,7 @@ use crate::{
         file_command::codeblock_fence_for_path,
         SlashCommandCompletionProvider, SlashCommandRegistry,
     },
+    slash_command_picker,
     terminal_inline_assistant::TerminalInlineAssistant,
     Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
     DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId, InlineAssistant,
@@ -1718,6 +1719,7 @@ pub struct ContextEditor {
     assistant_panel: WeakView<AssistantPanel>,
     error_message: Option<SharedString>,
     show_accept_terms: bool,
+    slash_menu_handle: PopoverMenuHandle<ContextMenu>,
 }
 
 const DEFAULT_TAB_TITLE: &str = "New Context";
@@ -1779,6 +1781,7 @@ impl ContextEditor {
             assistant_panel,
             error_message: None,
             show_accept_terms: false,
+            slash_menu_handle: Default::default(),
         };
         this.update_message_headers(cx);
         this.update_image_blocks(cx);
@@ -2007,7 +2010,7 @@ impl ContextEditor {
             .collect()
     }
 
-    fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
+    pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
         if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
             self.editor.update(cx, |editor, cx| {
                 editor.transact(cx, |editor, cx| {
@@ -3589,11 +3592,11 @@ impl ContextEditor {
                 button.tooltip(move |_| tooltip.clone())
             })
             .layer(ElevationIndex::ModalSurface)
+            .child(Label::new(button_text))
             .children(
                 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
                     .map(|binding| binding.into_any_element()),
             )
-            .child(Label::new(button_text))
             .on_click(move |_event, cx| {
                 focus_handle.dispatch_action(&Assist, cx);
             })
@@ -3623,7 +3626,13 @@ impl Render for ContextEditor {
         } else {
             None
         };
-
+        let focus_handle = self
+            .workspace
+            .update(cx, |workspace, cx| {
+                Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
+            })
+            .ok()
+            .flatten();
         v_flex()
             .key_context("ContextEditor")
             .capture_action(cx.listener(ContextEditor::cancel))
@@ -3700,14 +3709,47 @@ impl Render for ContextEditor {
                 )
             })
             .child(
-                h_flex().flex_none().relative().child(
+                h_flex().w_full().relative().child(
                     h_flex()
+                        .p_2()
                         .w_full()
-                        .absolute()
-                        .right_4()
-                        .bottom_2()
-                        .justify_end()
-                        .child(self.render_send_button(cx)),
+                        .border_t_1()
+                        .border_color(cx.theme().colors().border_variant)
+                        .bg(cx.theme().colors().editor_background)
+                        .child(
+                            h_flex()
+                                .gap_2()
+                                .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")
+                                                    .meta("Press to quote via keyboard")
+                                                    .key_binding(focus_handle.as_ref().and_then(
+                                                        |handle| {
+                                                            KeyBinding::for_action_in(
+                                                                &QuoteSelection,
+                                                                &handle,
+                                                                cx,
+                                                            )
+                                                        },
+                                                    ))
+                                            })
+                                            .into()
+                                        }),
+                                ),
+                        )
+                        .child(
+                            h_flex()
+                                .w_full()
+                                .justify_end()
+                                .child(div().child(self.render_send_button(cx))),
+                        ),
                 ),
             )
     }
@@ -3956,6 +3998,37 @@ pub struct ContextEditorToolbarItem {
     model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
 }
 
+fn active_editor_focus_handle(
+    workspace: &WeakView<Workspace>,
+    cx: &WindowContext<'_>,
+) -> Option<FocusHandle> {
+    workspace.upgrade().and_then(|workspace| {
+        Some(
+            workspace
+                .read(cx)
+                .active_item_as::<Editor>(cx)?
+                .focus_handle(cx),
+        )
+    })
+}
+
+fn render_inject_context_menu(
+    active_context_editor: WeakView<ContextEditor>,
+    cx: &mut WindowContext<'_>,
+) -> impl IntoElement {
+    let commands = SlashCommandRegistry::global(cx);
+
+    slash_command_picker::SlashCommandSelector::new(
+        commands.clone(),
+        active_context_editor,
+        IconButton::new("trigger", IconName::SlashSquare)
+            .icon_size(IconSize::Small)
+            .tooltip(|cx| {
+                Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
+            }),
+    )
+}
+
 impl ContextEditorToolbarItem {
     pub fn new(
         workspace: &Workspace,
@@ -3971,70 +4044,6 @@ impl ContextEditorToolbarItem {
         }
     }
 
-    fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
-        let commands = SlashCommandRegistry::global(cx);
-        let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
-            Some(
-                workspace
-                    .read(cx)
-                    .active_item_as::<Editor>(cx)?
-                    .focus_handle(cx),
-            )
-        });
-        let active_context_editor = self.active_context_editor.clone();
-
-        PopoverMenu::new("inject-context-menu")
-            .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
-                Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
-            }))
-            .menu(move |cx| {
-                let active_context_editor = active_context_editor.clone()?;
-                ContextMenu::build(cx, |mut menu, _cx| {
-                    for command_name in commands.featured_command_names() {
-                        if let Some(command) = commands.command(&command_name) {
-                            let menu_text = SharedString::from(Arc::from(command.menu_text()));
-                            menu = menu.custom_entry(
-                                {
-                                    let command_name = command_name.clone();
-                                    move |_cx| {
-                                        h_flex()
-                                            .gap_4()
-                                            .w_full()
-                                            .justify_between()
-                                            .child(Label::new(menu_text.clone()))
-                                            .child(
-                                                Label::new(format!("/{command_name}"))
-                                                    .color(Color::Muted),
-                                            )
-                                            .into_any()
-                                    }
-                                },
-                                {
-                                    let active_context_editor = active_context_editor.clone();
-                                    move |cx| {
-                                        active_context_editor
-                                            .update(cx, |context_editor, cx| {
-                                                context_editor.insert_command(&command_name, cx)
-                                            })
-                                            .ok();
-                                    }
-                                },
-                            )
-                        }
-                    }
-
-                    if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
-                        menu = menu
-                            .context(active_editor_focus_handle)
-                            .action("Quote Selection", Box::new(QuoteSelection));
-                    }
-
-                    menu
-                })
-                .into()
-            })
-    }
-
     fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
         let context = &self
             .active_context_editor
@@ -4081,24 +4090,16 @@ impl ContextEditorToolbarItem {
 impl Render for ContextEditorToolbarItem {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let left_side = h_flex()
+            .pl_1()
             .gap_2()
             .flex_1()
             .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
             .when(self.active_context_editor.is_some(), |left_side| {
-                left_side
-                    .child(
-                        IconButton::new("regenerate-context", IconName::ArrowCircle)
-                            .visible_on_hover("toolbar")
-                            .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
-                            .on_click(cx.listener(move |_, _, cx| {
-                                cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
-                            })),
-                    )
-                    .child(self.model_summary_editor.clone())
+                left_side.child(self.model_summary_editor.clone())
             });
         let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
         let active_model = LanguageModelRegistry::read_global(cx).active_model();
-
+        let weak_self = cx.view().downgrade();
         let right_side = h_flex()
             .gap_2()
             .child(
@@ -4148,7 +4149,70 @@ impl Render for ContextEditorToolbarItem {
                 .with_handle(self.model_selector_menu_handle.clone()),
             )
             .children(self.render_remaining_tokens(cx))
-            .child(self.render_inject_context_menu(cx));
+            .child(
+                PopoverMenu::new("context-editor-popover")
+                    .trigger(
+                        IconButton::new("context-editor-trigger", IconName::EllipsisVertical)
+                            .icon_size(IconSize::Small)
+                            .tooltip(|cx| Tooltip::text("Open Context Options", cx)),
+                    )
+                    .menu({
+                        let weak_self = weak_self.clone();
+                        move |cx| {
+                            let weak_self = weak_self.clone();
+                            Some(ContextMenu::build(cx, move |menu, cx| {
+                                let context = weak_self
+                                    .update(cx, |this, cx| {
+                                        active_editor_focus_handle(&this.workspace, cx)
+                                    })
+                                    .ok()
+                                    .flatten();
+                                menu.when_some(context, |menu, context| menu.context(context))
+                                    .entry("Regenerate Context Title", None, {
+                                        let weak_self = weak_self.clone();
+                                        move |cx| {
+                                            weak_self
+                                                .update(cx, |_, cx| {
+                                                    cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
+                                                })
+                                                .ok();
+                                        }
+                                    })
+                                    .custom_entry(
+                                        |_| {
+                                            h_flex()
+                                                .w_full()
+                                                .justify_between()
+                                                .gap_2()
+                                                .child(Label::new("Insert Context"))
+                                                .child(Label::new("/ command").color(Color::Muted))
+                                                .into_any()
+                                        },
+                                        {
+                                            let weak_self = weak_self.clone();
+                                            move |cx| {
+                                                weak_self
+                                                    .update(cx, |this, cx| {
+                                                        if let Some(editor) =
+                                                        &this.active_context_editor
+                                                        {
+                                                            editor
+                                                                .update(cx, |this, cx| {
+                                                                    this.slash_menu_handle
+                                                                        .toggle(cx);
+                                                                })
+                                                                .ok();
+                                                        }
+                                                    })
+                                                    .ok();
+                                            }
+                                        },
+                                    )
+                                    .action("Insert Selection", QuoteSelection.boxed_clone())
+                            }))
+                        }
+                    }),
+            );
 
         h_flex()
             .size_full()

crates/assistant/src/slash_command_picker.rs 🔗

@@ -0,0 +1,201 @@
+use assistant_slash_command::SlashCommandRegistry;
+use gpui::DismissEvent;
+use gpui::WeakView;
+use picker::PickerEditorPosition;
+
+use std::sync::Arc;
+use ui::ListItemSpacing;
+
+use gpui::SharedString;
+use gpui::Task;
+use picker::{Picker, PickerDelegate};
+use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
+
+use crate::assistant_panel::ContextEditor;
+
+#[derive(IntoElement)]
+pub struct SlashCommandSelector<T: PopoverTrigger> {
+    handle: Option<PopoverMenuHandle<Picker<SlashCommandDelegate>>>,
+    registry: Arc<SlashCommandRegistry>,
+    active_context_editor: WeakView<ContextEditor>,
+    trigger: T,
+    info_text: Option<SharedString>,
+}
+
+#[derive(Clone)]
+struct SlashCommandInfo {
+    name: SharedString,
+    description: SharedString,
+}
+
+pub struct SlashCommandDelegate {
+    all_commands: Vec<SlashCommandInfo>,
+    filtered_commands: Vec<SlashCommandInfo>,
+    active_context_editor: WeakView<ContextEditor>,
+    selected_index: usize,
+}
+
+impl<T: PopoverTrigger> SlashCommandSelector<T> {
+    pub fn new(
+        registry: Arc<SlashCommandRegistry>,
+        active_context_editor: WeakView<ContextEditor>,
+        trigger: T,
+    ) -> Self {
+        SlashCommandSelector {
+            handle: None,
+            registry,
+            active_context_editor,
+            trigger,
+            info_text: None,
+        }
+    }
+
+    pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<SlashCommandDelegate>>) -> Self {
+        self.handle = Some(handle);
+        self
+    }
+
+    pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
+        self.info_text = Some(text.into());
+        self
+    }
+}
+
+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, cx: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
+        cx.notify();
+    }
+
+    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+        "Select a command...".into()
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        let all_commands = self.all_commands.clone();
+        cx.spawn(|this, mut cx| async move {
+            let filtered_commands = cx
+                .background_executor()
+                .spawn(async move {
+                    if query.is_empty() {
+                        all_commands
+                    } else {
+                        all_commands
+                            .into_iter()
+                            .filter(|model_info| {
+                                model_info
+                                    .name
+                                    .to_lowercase()
+                                    .contains(&query.to_lowercase())
+                            })
+                            .collect()
+                    }
+                })
+                .await;
+
+            this.update(&mut cx, |this, cx| {
+                this.delegate.filtered_commands = filtered_commands;
+                this.delegate.set_selected_index(0, cx);
+                cx.notify();
+            })
+            .ok();
+        })
+    }
+
+    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
+        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();
+            cx.emit(DismissEvent);
+        }
+    }
+
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
+
+    fn editor_position(&self) -> PickerEditorPosition {
+        PickerEditorPosition::End
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        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()
+                            .child(
+                                Label::new(format!("/{}", command_info.name))
+                                    .size(LabelSize::Small),
+                            )
+                            .child(
+                                Label::new(command_info.description.clone())
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            ),
+                    ),
+                ),
+        )
+    }
+}
+
+impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let all_models = self
+            .registry
+            .featured_command_names()
+            .into_iter()
+            .filter_map(|command_name| {
+                let command = self.registry.command(&command_name)?;
+                let menu_text = SharedString::from(Arc::from(command.menu_text()));
+                Some(SlashCommandInfo {
+                    name: command_name.into(),
+                    description: menu_text,
+                })
+            })
+            .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_view(|cx| {
+            let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
+            picker
+        });
+
+        PopoverMenu::new("model-switcher")
+            .menu(move |_cx| Some(picker_view.clone()))
+            .trigger(self.trigger)
+            .attach(gpui::AnchorCorner::TopLeft)
+            .anchor(gpui::AnchorCorner::BottomLeft)
+            .offset(gpui::Point {
+                x: px(0.0),
+                y: px(-16.0),
+            })
+    }
+}

crates/picker/src/picker.rs 🔗

@@ -51,6 +51,15 @@ pub struct Picker<D: PickerDelegate> {
     is_modal: bool,
 }
 
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub enum PickerEditorPosition {
+    #[default]
+    /// Render the editor at the start of the picker. Usually the top
+    Start,
+    /// Render the editor at the end of the picker. Usually the bottom
+    End,
+}
+
 pub trait PickerDelegate: Sized + 'static {
     type ListItem: IntoElement;
 
@@ -103,8 +112,16 @@ pub trait PickerDelegate: Sized + 'static {
         None
     }
 
+    fn editor_position(&self) -> PickerEditorPosition {
+        PickerEditorPosition::default()
+    }
+
     fn render_editor(&self, editor: &View<Editor>, _cx: &mut ViewContext<Picker<Self>>) -> Div {
         v_flex()
+            .when(
+                self.editor_position() == PickerEditorPosition::End,
+                |this| this.child(Divider::horizontal()),
+            )
             .child(
                 h_flex()
                     .overflow_hidden()
@@ -113,7 +130,10 @@ pub trait PickerDelegate: Sized + 'static {
                     .px_3()
                     .child(editor.clone()),
             )
-            .child(Divider::horizontal())
+            .when(
+                self.editor_position() == PickerEditorPosition::Start,
+                |this| this.child(Divider::horizontal()),
+            )
     }
 
     fn render_match(
@@ -555,6 +575,8 @@ impl<D: PickerDelegate> ModalView for Picker<D> {}
 
 impl<D: PickerDelegate> Render for Picker<D> {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let editor_position = self.delegate.editor_position();
+
         v_flex()
             .key_context("Picker")
             .size_full()
@@ -574,9 +596,15 @@ impl<D: PickerDelegate> Render for Picker<D> {
             .on_action(cx.listener(Self::secondary_confirm))
             .on_action(cx.listener(Self::confirm_completion))
             .on_action(cx.listener(Self::confirm_input))
-            .child(match &self.head {
-                Head::Editor(editor) => self.delegate.render_editor(&editor.clone(), cx),
-                Head::Empty(empty_head) => div().child(empty_head.clone()),
+            .children(match &self.head {
+                Head::Editor(editor) => {
+                    if editor_position == PickerEditorPosition::Start {
+                        Some(self.delegate.render_editor(&editor.clone(), cx))
+                    } else {
+                        None
+                    }
+                }
+                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
             })
             .when(self.delegate.match_count() > 0, |el| {
                 el.child(
@@ -602,5 +630,15 @@ impl<D: PickerDelegate> Render for Picker<D> {
                 )
             })
             .children(self.delegate.render_footer(cx))
+            .children(match &self.head {
+                Head::Editor(editor) => {
+                    if editor_position == PickerEditorPosition::End {
+                        Some(self.delegate.render_editor(&editor.clone(), cx))
+                    } else {
+                        None
+                    }
+                }
+                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
+            })
     }
 }

crates/ui/src/components/icon.rs 🔗

@@ -157,6 +157,7 @@ pub enum IconName {
     Disconnected,
     Download,
     Ellipsis,
+    EllipsisVertical,
     Envelope,
     Escape,
     ExclamationTriangle,
@@ -233,6 +234,8 @@ pub enum IconName {
     Server,
     Settings,
     Shift,
+    Slash,
+    SlashSquare,
     Sliders,
     SlidersAlt,
     Snip,
@@ -320,6 +323,7 @@ impl IconName {
             IconName::Disconnected => "icons/disconnected.svg",
             IconName::Download => "icons/download.svg",
             IconName::Ellipsis => "icons/ellipsis.svg",
+            IconName::EllipsisVertical => "icons/ellipsis_vertical.svg",
             IconName::Envelope => "icons/feedback.svg",
             IconName::Escape => "icons/escape.svg",
             IconName::ExclamationTriangle => "icons/warning.svg",
@@ -396,6 +400,8 @@ impl IconName {
             IconName::Server => "icons/server.svg",
             IconName::Settings => "icons/file_icons/settings.svg",
             IconName::Shift => "icons/shift.svg",
+            IconName::Slash => "icons/slash.svg",
+            IconName::SlashSquare => "icons/slash_square.svg",
             IconName::Sliders => "icons/sliders.svg",
             IconName::SlidersAlt => "icons/sliders-alt.svg",
             IconName::Snip => "icons/snip.svg",