1use std::rc::Rc;
5
6use gpui_kit::{Action, AnyElement, App, FocusHandle, IntoElement, SharedString, Window};
7
8use crate::Icon;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct CommandRow {
15 pub(crate) highlighted: bool,
16 pub(crate) disabled: bool,
17}
18
19impl CommandRow {
20 pub fn is_highlighted(&self) -> bool {
22 self.highlighted
23 }
24
25 pub fn is_disabled(&self) -> bool {
27 self.disabled
28 }
29}
30
31pub(crate) type RowRenderer = Rc<dyn Fn(CommandRow, &mut Window, &mut App) -> AnyElement>;
32
33#[non_exhaustive]
35pub struct CommandItem {
36 key: SharedString,
37 label: SharedString,
38 icon: Option<Box<Icon>>,
41 keywords: Vec<SharedString>,
43 action: Option<Box<dyn Action>>,
44 action_context: Option<FocusHandle>,
45 disabled: bool,
46 render: Option<RowRenderer>,
47}
48
49impl Clone for CommandItem {
50 fn clone(&self) -> Self {
51 Self {
52 key: self.key.clone(),
53 label: self.label.clone(),
54 icon: self.icon.clone(),
55 keywords: self.keywords.clone(),
56 action: self.action.as_ref().map(|action| action.boxed_clone()),
57 action_context: self.action_context.clone(),
58 disabled: self.disabled,
59 render: self.render.clone(),
60 }
61 }
62}
63
64impl CommandItem {
65 pub fn new(key: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
67 let label = label.into();
68 Self {
69 key: key.into(),
70 keywords: Vec::new(),
71 label,
72 icon: None,
73 action: None,
74 action_context: None,
75 disabled: false,
76 render: None,
77 }
78 }
79
80 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
82 self.icon = Some(Box::new(icon.into()));
83 self
84 }
85
86 pub fn keywords(mut self, keywords: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
89 for keyword in keywords {
90 self.keywords.push(keyword.into());
91 }
92 self
93 }
94
95 pub fn action(mut self, action: impl Action) -> Self {
98 self.action = Some(Box::new(action));
99 self
100 }
101
102 pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
105 self.action_context = Some(focus_handle.clone());
106 self
107 }
108
109 pub fn disabled(mut self, disabled: bool) -> Self {
112 self.disabled = disabled;
113 self
114 }
115
116 pub fn render<E: IntoElement>(
123 mut self,
124 render: impl Fn(CommandRow, &mut Window, &mut App) -> E + 'static,
125 ) -> Self {
126 self.render = Some(Rc::new(move |row, window, cx| {
127 render(row, window, cx).into_any_element()
128 }));
129 self
130 }
131
132 pub fn key(&self) -> &SharedString {
134 &self.key
135 }
136
137 pub fn label(&self) -> &SharedString {
139 &self.label
140 }
141
142 pub fn is_disabled(&self) -> bool {
144 self.disabled
145 }
146
147 pub(crate) fn matches(&self, query: &str) -> bool {
150 self.match_score(query).is_some()
151 }
152
153 pub(crate) fn match_score(&self, query: &str) -> Option<u32> {
156 let label = crate::fuzzy::score_of(query, &self.label);
157 let keyword = self
158 .keywords
159 .iter()
160 .filter_map(|keyword| crate::fuzzy::score_of(query, keyword))
161 .max()
162 .map(|score| score / 2);
163 label.max(keyword)
164 }
165
166 pub(crate) fn leading_icon(&self) -> Option<&Icon> {
167 self.icon.as_deref()
168 }
169
170 pub(crate) fn dispatched_action(&self) -> Option<&dyn Action> {
171 self.action.as_deref()
172 }
173
174 pub(crate) fn own_action_context(&self) -> Option<&FocusHandle> {
175 self.action_context.as_ref()
176 }
177
178 pub(crate) fn renderer(&self) -> Option<&RowRenderer> {
179 self.render.as_ref()
180 }
181}
182
183#[derive(Clone)]
186#[non_exhaustive]
187pub struct CommandGroup {
188 heading: Option<SharedString>,
189 items: Vec<CommandItem>,
190}
191
192impl CommandGroup {
193 pub fn new() -> Self {
195 Self {
196 heading: None,
197 items: Vec::new(),
198 }
199 }
200
201 pub fn heading(mut self, heading: impl Into<SharedString>) -> Self {
203 self.heading = Some(heading.into());
204 self
205 }
206
207 pub fn items(mut self, items: impl IntoIterator<Item = CommandItem>) -> Self {
209 self.items.extend(items);
210 self
211 }
212}
213
214impl Default for CommandGroup {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220#[derive(Clone)]
222pub enum CommandEntry {
223 Item(CommandItem),
225 Group(CommandGroup),
227 Separator,
230}
231
232impl From<CommandItem> for CommandEntry {
233 fn from(item: CommandItem) -> Self {
234 Self::Item(item)
235 }
236}
237
238impl From<CommandGroup> for CommandEntry {
239 fn from(group: CommandGroup) -> Self {
240 Self::Group(group)
241 }
242}
243
244#[derive(Clone)]
247pub(crate) enum Row {
248 Heading(SharedString),
249 Separator,
250 Item(CommandItem),
251}
252
253pub(crate) fn rows(entries: &[CommandEntry], query: &str) -> Vec<Row> {
257 let mut rows = Vec::new();
258 let mut pending_separator = false;
259 let push = |block: Vec<Row>, rows: &mut Vec<Row>, separator: &mut bool| {
260 if block.is_empty() {
261 return;
262 }
263 if *separator && !rows.is_empty() {
264 rows.push(Row::Separator);
265 }
266 *separator = false;
267 rows.extend(block);
268 };
269 for entry in entries {
270 match entry {
271 CommandEntry::Separator => pending_separator = true,
272 CommandEntry::Item(item) => {
273 let block = if item.matches(query) {
274 vec![Row::Item(item.clone())]
275 } else {
276 Vec::new()
277 };
278 push(block, &mut rows, &mut pending_separator);
279 }
280 CommandEntry::Group(group) => {
281 let mut hits: Vec<(&CommandItem, u32)> = group
282 .items
283 .iter()
284 .filter_map(|item| Some((item, item.match_score(query)?)))
285 .collect();
286 hits.sort_by_key(|(_, score)| std::cmp::Reverse(*score));
287 let items: Vec<Row> = hits
288 .into_iter()
289 .map(|(item, _)| Row::Item(item.clone()))
290 .collect();
291 let block = if items.is_empty() {
292 items
293 } else {
294 group
295 .heading
296 .clone()
297 .map(Row::Heading)
298 .into_iter()
299 .chain(items)
300 .collect()
301 };
302 push(block, &mut rows, &mut pending_separator);
303 }
304 }
305 }
306 rows
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 fn shape(rows: &[Row]) -> Vec<String> {
314 rows.iter()
315 .map(|row| match row {
316 Row::Heading(text) => format!("# {text}"),
317 Row::Separator => "--".into(),
318 Row::Item(item) => item.key().to_string(),
319 })
320 .collect()
321 }
322
323 #[test]
324 fn a_query_ranks_the_commands_of_a_group_best_first() {
325 let entries = vec![
326 CommandGroup::new()
327 .heading("Language")
328 .items([
329 CommandItem::new("de", "Deutsch").keywords(["German"]),
330 CommandItem::new("en", "English"),
331 ])
332 .into(),
333 ];
334 assert_eq!(shape(&rows(&entries, "")), ["# Language", "de", "en"]);
335 assert_eq!(shape(&rows(&entries, "en")), ["# Language", "en", "de"]);
336 }
337
338 #[test]
339 fn a_query_matches_labels_and_keywords_and_drops_empty_groups() {
340 let entries = vec![
341 CommandItem::new("project", "Project").into(),
342 CommandEntry::Separator,
343 CommandGroup::new()
344 .heading("Open")
345 .items([
346 CommandItem::new("terminal", "Terminal").keywords(["Shell"]),
347 CommandItem::new("browser", "Browser"),
348 ])
349 .into(),
350 CommandEntry::Separator,
351 CommandItem::new("desktop", "Desktop").into(),
352 ];
353 assert_eq!(
354 shape(&rows(&entries, "")),
355 [
356 "project", "--", "# Open", "terminal", "browser", "--", "desktop"
357 ]
358 );
359 assert_eq!(shape(&rows(&entries, "shell")), ["# Open", "terminal"]);
360 assert_eq!(
361 shape(&rows(&entries, "o")),
362 ["project", "--", "# Open", "browser", "--", "desktop"]
363 );
364 assert!(rows(&entries, "zzz").is_empty());
365 }
366}