1use std::rc::Rc;
9
10use gpui::{App, Global, SharedString, WeakEntity, Window};
11use ui::{
12 icons::{Icon, glyph},
13 menu::{self, Item},
14 popover::filter_indices,
15};
16
17use markdown::{Align, BlockKind, Cursor, QuoteKind, Text};
18
19pub fn items() -> Vec<(SharedString, BlockKind)> {
26 let text = Text::default;
27 vec![
28 ("Text".into(), BlockKind::Paragraph(text())),
29 (
30 "Heading 1".into(),
31 BlockKind::Heading {
32 level: 1,
33 text: text(),
34 },
35 ),
36 (
37 "Heading 2".into(),
38 BlockKind::Heading {
39 level: 2,
40 text: text(),
41 },
42 ),
43 (
44 "Heading 3".into(),
45 BlockKind::Heading {
46 level: 3,
47 text: text(),
48 },
49 ),
50 ("Bullet".into(), BlockKind::Bullet(text())),
51 (
52 "Numbered".into(),
53 BlockKind::Ordered {
54 number: 1,
55 text: text(),
56 },
57 ),
58 (
59 "Task".into(),
60 BlockKind::Task {
61 checked: false,
62 text: text(),
63 },
64 ),
65 (
66 "Code".into(),
67 BlockKind::Code {
68 language: None,
69 code: text(),
70 height: None,
71 },
72 ),
73 (
74 "Table".into(),
75 BlockKind::Table {
76 align: vec![Align::Left; 2],
77 header: vec![text(), text()],
78 rows: vec![vec![text(), text()]],
79 },
80 ),
81 (
82 "Image".into(),
83 BlockKind::Image {
84 url: String::new(),
85 alt: text(),
86 width: None,
87 },
88 ),
89 ("Divider".into(), BlockKind::Rule),
90 ]
91}
92
93pub fn quotes() -> Vec<(SharedString, Option<QuoteKind>)> {
95 vec![
96 ("Quote".into(), None),
97 ("Note".into(), Some(QuoteKind::Note)),
98 ("Tip".into(), Some(QuoteKind::Tip)),
99 ("Important".into(), Some(QuoteKind::Important)),
100 ("Warning".into(), Some(QuoteKind::Warning)),
101 ("Caution".into(), Some(QuoteKind::Caution)),
102 ]
103}
104
105pub type SlashRun = Rc<dyn Fn(SlashAt, &mut Window, &mut App)>;
107
108#[derive(Clone)]
110pub enum SlashAction {
111 Block(BlockKind),
113 Quote(Option<QuoteKind>),
116 Run(SlashRun),
120}
121
122#[derive(Clone)]
124pub struct SlashAt {
125 pub editor: WeakEntity<crate::Editor>,
126 pub block: usize,
127}
128
129#[derive(Clone)]
131pub struct SlashRow {
132 pub label: SharedString,
133 pub icon: Option<Icon>,
134 pub action: SlashAction,
135}
136
137#[derive(Clone)]
139pub enum SlashItem {
140 Heading(SharedString),
143 Row(SlashRow),
144 Group {
147 label: SharedString,
148 icon: Option<Icon>,
149 rows: Vec<SlashRow>,
150 },
151}
152
153pub fn defaults() -> Vec<SlashItem> {
156 let mut defaults = Vec::new();
157 for (label, kind) in items() {
158 let lists_end = matches!(kind, BlockKind::Code { .. });
160 if lists_end {
161 defaults.push(SlashItem::Group {
162 label: "Quote".into(),
163 icon: Some(glyph::TextQuote.into()),
164 rows: quotes()
165 .into_iter()
166 .map(|(label, alert)| SlashRow {
167 label,
168 icon: Some(quote_glyph(alert)),
169 action: SlashAction::Quote(alert),
170 })
171 .collect(),
172 });
173 }
174 defaults.push(SlashItem::Row(SlashRow {
175 label,
176 icon: Some(glyph_of(&kind)),
177 action: SlashAction::Block(kind),
178 }));
179 }
180 defaults
181}
182
183fn glyph_of(kind: &BlockKind) -> Icon {
185 match kind {
186 BlockKind::Paragraph(_) => glyph::Pilcrow.into(),
187 BlockKind::Heading { level: 1, .. } => glyph::Heading1.into(),
188 BlockKind::Heading { level: 2, .. } => glyph::Heading2.into(),
189 BlockKind::Heading { .. } => glyph::Heading3.into(),
190 BlockKind::Bullet(_) => glyph::List.into(),
191 BlockKind::Ordered { .. } => glyph::ListOrdered.into(),
192 BlockKind::Task { .. } => glyph::ListTodo.into(),
193 BlockKind::Code { .. } => glyph::SquareCode.into(),
194 BlockKind::Table { .. } => glyph::Table.into(),
195 BlockKind::Image { .. } => glyph::Image.into(),
196 _ => glyph::SeparatorHorizontal.into(),
197 }
198}
199
200fn quote_glyph(alert: Option<QuoteKind>) -> Icon {
202 match alert {
203 None => glyph::TextQuote.into(),
204 Some(QuoteKind::Note) => glyph::Info.into(),
205 Some(QuoteKind::Tip) => glyph::Lightbulb.into(),
206 Some(QuoteKind::Important) => glyph::MessageSquareWarning.into(),
207 Some(QuoteKind::Warning) => glyph::TriangleAlert.into(),
208 Some(QuoteKind::Caution) => glyph::OctagonAlert.into(),
209 }
210}
211
212pub(crate) struct Installed(pub Vec<SlashItem>);
214
215impl Global for Installed {}
216
217pub(crate) fn installed(cx: &App) -> Vec<SlashItem> {
219 cx.try_global::<Installed>()
220 .map_or_else(defaults, |Installed(items)| items.clone())
221}
222
223#[derive(Clone)]
227struct Entry {
228 label: SharedString,
229 short: SharedString,
230 section: Option<(usize, SharedString)>,
232 group: Option<(SharedString, Option<Icon>)>,
233 icon: Option<Icon>,
234 action: SlashAction,
235}
236
237fn entries(items: Vec<SlashItem>) -> Vec<Entry> {
239 let mut entries = Vec::new();
240 let mut section = None;
241 for (ix, item) in items.into_iter().enumerate() {
242 match item {
243 SlashItem::Heading(label) => section = Some((ix, label)),
244 SlashItem::Row(row) => entries.push(Entry {
245 label: row.label.clone(),
246 short: row.label,
247 section: section.clone(),
248 group: None,
249 icon: row.icon,
250 action: row.action,
251 }),
252 SlashItem::Group { label, icon, rows } => {
253 entries.extend(rows.into_iter().map(|row| Entry {
254 label: match row.label == label {
255 true => label.clone(),
256 false => format!("{label} ({})", row.label).into(),
257 },
258 short: row.label,
259 section: section.clone(),
260 group: Some((label.clone(), icon.clone())),
261 icon: row.icon,
262 action: row.action,
263 }))
264 }
265 }
266 }
267 entries
268}
269
270pub(crate) fn turns(items: Vec<SlashItem>) -> Vec<SlashItem> {
273 let turns =
274 |row: &SlashRow| matches!(row.action, SlashAction::Block(_) | SlashAction::Quote(_));
275 items
276 .into_iter()
277 .filter_map(|item| match item {
278 SlashItem::Row(row) => turns(&row).then_some(SlashItem::Row(row)),
279 SlashItem::Group { label, icon, rows } => {
280 let rows: Vec<SlashRow> = rows.into_iter().filter(turns).collect();
281 (!rows.is_empty()).then_some(SlashItem::Group { label, icon, rows })
282 }
283 SlashItem::Heading(_) => None,
284 })
285 .collect()
286}
287
288pub fn label(kind: &BlockKind) -> Option<SharedString> {
295 items()
296 .into_iter()
297 .find(|(_, row)| same(row, kind))
298 .map(|(label, _)| label)
299}
300
301fn same(row: &BlockKind, kind: &BlockKind) -> bool {
302 match (row, kind) {
303 (BlockKind::Heading { level: a, .. }, BlockKind::Heading { level: b, .. }) => a == b,
304 (row, kind) => std::mem::discriminant(row) == std::mem::discriminant(kind),
305 }
306}
307
308enum Row {
311 Heading(SharedString),
312 Block(usize),
313 Group(SharedString, Option<Icon>, Vec<usize>),
314}
315
316pub struct Rows {
318 rows: Vec<Row>,
319 entries: Vec<Entry>,
321}
322
323impl Rows {
324 pub fn new(items: Vec<SlashItem>) -> Self {
325 let mut rows = Self {
326 rows: Vec::new(),
327 entries: entries(items),
328 };
329 rows.refilter("");
330 rows
331 }
332
333 pub fn refilter(&mut self, query: &str) {
338 let order: Vec<usize> = if query.is_empty() {
339 (0..self.entries.len()).collect()
340 } else {
341 let labels: Vec<SharedString> = self
342 .entries
343 .iter()
344 .map(|entry| entry.label.clone())
345 .collect();
346 filter_indices(query, &labels)
347 };
348 let mut rows: Vec<Row> = Vec::new();
349 let mut section = None;
350 for ix in order {
351 let entry = &self.entries[ix];
352 if query.is_empty() && entry.section.as_ref().map(|(at, _)| *at) != section {
353 section = entry.section.as_ref().map(|(at, _)| *at);
354 if let Some((_, label)) = &entry.section {
355 rows.push(Row::Heading(label.clone()));
356 }
357 }
358 let Some((group, icon)) = &entry.group else {
359 rows.push(Row::Block(ix));
360 continue;
361 };
362 match rows
363 .iter_mut()
364 .find(|row| matches!(row, Row::Group(label, ..) if label == group))
365 {
366 Some(Row::Group(_, _, held)) => held.push(ix),
367 _ => rows.push(Row::Group(group.clone(), icon.clone(), vec![ix])),
368 }
369 }
370 self.rows = rows
371 .into_iter()
372 .map(|row| match row {
373 Row::Group(_, _, held) if !query.is_empty() && held.len() == 1 => {
374 Row::Block(held[0])
375 }
376 row => row,
377 })
378 .collect();
379 }
380
381 pub fn menu(&self) -> Vec<Item> {
383 self.rows
384 .iter()
385 .map(|row| match row {
386 Row::Heading(label) => Item::Heading(label.clone()),
387 Row::Block(ix) => self.item(*ix, &self.entries[*ix].label),
388 Row::Group(label, icon, group) => {
389 let submenu = Item::submenu(
390 label.clone(),
391 group
392 .iter()
393 .map(|ix| self.item(*ix, &self.entries[*ix].short))
394 .collect(),
395 );
396 match icon.clone() {
397 Some(icon) => submenu.with_icon(icon),
398 None => submenu,
399 }
400 }
401 })
402 .collect()
403 }
404
405 fn item(&self, ix: usize, label: &SharedString) -> Item {
406 let item = Item::action(label.clone());
407 match self.entries[ix].icon.clone() {
408 Some(icon) => item.with_icon(icon),
409 None => item,
410 }
411 }
412
413 fn is_group(&self, path: &[usize]) -> bool {
416 matches!(
417 (path.first().and_then(|row| self.rows.get(*row)), path.len()),
418 (Some(Row::Group(..)), 1)
419 )
420 }
421
422 pub fn action_at(&self, path: &[usize]) -> Option<SlashAction> {
424 let ix = match (self.rows.get(*path.first()?)?, path.get(1)) {
425 (Row::Block(ix), None) => *ix,
426 (Row::Group(_, _, group), Some(row)) => *group.get(*row)?,
427 _ => return None,
428 };
429 self.entries.get(ix).map(|entry| entry.action.clone())
430 }
431}
432
433pub struct Slash {
435 pub at: Cursor,
438 rows: Rows,
439 pub cursor: menu::Cursor,
440}
441
442impl Slash {
443 pub fn open(at: Cursor, items: Vec<SlashItem>) -> Self {
444 let mut slash = Self {
445 at,
446 rows: Rows::new(items),
447 cursor: menu::Cursor::default(),
448 };
449 slash.refilter("");
450 slash
451 }
452
453 pub fn refilter(&mut self, query: &str) {
454 self.rows.refilter(query);
455 self.cursor.clear();
456 self.cursor.step(&self.menu(), 1);
457 }
458
459 pub fn menu(&self) -> Vec<Item> {
460 self.rows.menu()
461 }
462
463 pub fn step(&mut self, delta: isize) {
465 let menu = self.menu();
466 self.cursor.step(&menu, delta);
467 }
468
469 pub fn descend(&mut self) -> bool {
471 let menu = self.menu();
472 self.cursor.descend(&menu)
473 }
474
475 pub fn on_group(&self) -> bool {
477 self.cursor
478 .path()
479 .is_some_and(|path| self.rows.is_group(&path))
480 }
481
482 pub fn action_at(&self, path: &[usize]) -> Option<SlashAction> {
483 self.rows.action_at(path)
484 }
485
486 pub fn query(&self, caret: Cursor, text: &str) -> Option<String> {
489 if caret.block != self.at.block || caret.part != self.at.part {
490 return None;
491 }
492 let start = self.at.offset + 1;
493 if caret.offset < start {
494 return None;
495 }
496 let query = text.get(start..caret.offset)?;
497 (!query.contains(char::is_whitespace)).then(|| query.to_string())
499 }
500}