use std::rc::Rc;
use gpui_kit::{Action, AnyElement, App, FocusHandle, IntoElement, SharedString, Window};
use crate::Icon;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct CommandRow {
pub(crate) highlighted: bool,
pub(crate) disabled: bool,
}
impl CommandRow {
pub fn is_highlighted(&self) -> bool {
self.highlighted
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
}
pub(crate) type RowRenderer = Rc<dyn Fn(CommandRow, &mut Window, &mut App) -> AnyElement>;
#[non_exhaustive]
pub struct CommandItem {
key: SharedString,
label: SharedString,
icon: Option<Box<Icon>>,
search_text: String,
action: Option<Box<dyn Action>>,
action_context: Option<FocusHandle>,
disabled: bool,
render: Option<RowRenderer>,
}
impl Clone for CommandItem {
fn clone(&self) -> Self {
Self {
key: self.key.clone(),
label: self.label.clone(),
icon: self.icon.clone(),
search_text: self.search_text.clone(),
action: self.action.as_ref().map(|action| action.boxed_clone()),
action_context: self.action_context.clone(),
disabled: self.disabled,
render: self.render.clone(),
}
}
}
impl CommandItem {
pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
let label = label.into();
Self {
key: key.into(),
search_text: label.to_lowercase(),
label,
icon: None,
action: None,
action_context: None,
disabled: false,
render: None,
}
}
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(Box::new(icon.into()));
self
}
pub fn keywords(mut self, keywords: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
for keyword in keywords {
self.search_text.push('\n');
self.search_text.push_str(&keyword.into().to_lowercase());
}
self
}
pub fn action(mut self, action: impl Action) -> Self {
self.action = Some(Box::new(action));
self
}
pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
self.action_context = Some(focus_handle.clone());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn render<E: IntoElement>(
mut self,
render: impl Fn(CommandRow, &mut Window, &mut App) -> E + 'static,
) -> Self {
self.render = Some(Rc::new(move |row, window, cx| {
render(row, window, cx).into_any_element()
}));
self
}
pub fn key(&self) -> &SharedString {
&self.key
}
pub fn label(&self) -> &SharedString {
&self.label
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub(crate) fn matches(&self, query: &str) -> bool {
query.is_empty() || self.search_text.contains(query)
}
pub(crate) fn leading_icon(&self) -> Option<&Icon> {
self.icon.as_deref()
}
pub(crate) fn dispatched_action(&self) -> Option<&dyn Action> {
self.action.as_deref()
}
pub(crate) fn own_action_context(&self) -> Option<&FocusHandle> {
self.action_context.as_ref()
}
pub(crate) fn renderer(&self) -> Option<&RowRenderer> {
self.render.as_ref()
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct CommandGroup {
heading: Option<SharedString>,
items: Vec<CommandItem>,
}
impl CommandGroup {
pub fn new() -> Self {
Self {
heading: None,
items: Vec::new(),
}
}
pub fn heading(mut self, heading: impl Into<SharedString>) -> Self {
self.heading = Some(heading.into());
self
}
pub fn items(mut self, items: impl IntoIterator<Item = CommandItem>) -> Self {
self.items.extend(items);
self
}
}
impl Default for CommandGroup {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub enum CommandEntry {
Item(CommandItem),
Group(CommandGroup),
Separator,
}
impl From<CommandItem> for CommandEntry {
fn from(item: CommandItem) -> Self {
Self::Item(item)
}
}
impl From<CommandGroup> for CommandEntry {
fn from(group: CommandGroup) -> Self {
Self::Group(group)
}
}
#[derive(Clone)]
pub(crate) enum Row {
Heading(SharedString),
Separator,
Item(CommandItem),
}
pub(crate) fn rows(entries: &[CommandEntry], query: &str) -> Vec<Row> {
let mut rows = Vec::new();
let mut pending_separator = false;
let push = |block: Vec<Row>, rows: &mut Vec<Row>, separator: &mut bool| {
if block.is_empty() {
return;
}
if *separator && !rows.is_empty() {
rows.push(Row::Separator);
}
*separator = false;
rows.extend(block);
};
for entry in entries {
match entry {
CommandEntry::Separator => pending_separator = true,
CommandEntry::Item(item) => {
let block = if item.matches(query) {
vec![Row::Item(item.clone())]
} else {
Vec::new()
};
push(block, &mut rows, &mut pending_separator);
}
CommandEntry::Group(group) => {
let items: Vec<Row> = group
.items
.iter()
.filter(|item| item.matches(query))
.cloned()
.map(Row::Item)
.collect();
let block = if items.is_empty() {
items
} else {
group
.heading
.clone()
.map(Row::Heading)
.into_iter()
.chain(items)
.collect()
};
push(block, &mut rows, &mut pending_separator);
}
}
}
rows
}
#[cfg(test)]
mod tests {
use super::*;
fn shape(rows: &[Row]) -> Vec<String> {
rows.iter()
.map(|row| match row {
Row::Heading(text) => format!("# {text}"),
Row::Separator => "--".into(),
Row::Item(item) => item.key().to_string(),
})
.collect()
}
#[test]
fn a_query_matches_labels_and_keywords_and_drops_empty_groups() {
let entries = vec![
CommandItem::new("project", "Project").into(),
CommandEntry::Separator,
CommandGroup::new()
.heading("Open")
.items([
CommandItem::new("terminal", "Terminal").keywords(["Shell"]),
CommandItem::new("browser", "Browser"),
])
.into(),
CommandEntry::Separator,
CommandItem::new("desktop", "Desktop").into(),
];
assert_eq!(
shape(&rows(&entries, "")),
[
"project", "--", "# Open", "terminal", "browser", "--", "desktop"
]
);
assert_eq!(shape(&rows(&entries, "shell")), ["# Open", "terminal"]);
assert_eq!(
shape(&rows(&entries, "o")),
["project", "--", "# Open", "browser", "--", "desktop"]
);
assert!(rows(&entries, "zzz").is_empty());
}
}