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 "Quote".into(),
67 BlockKind::Quote {
68 kind: None,
69 text: text(),
70 },
71 ),
72 (
73 "Quote (Note)".into(),
74 BlockKind::Quote {
75 kind: Some(QuoteKind::Note),
76 text: text(),
77 },
78 ),
79 (
80 "Quote (Tip)".into(),
81 BlockKind::Quote {
82 kind: Some(QuoteKind::Tip),
83 text: text(),
84 },
85 ),
86 (
87 "Quote (Important)".into(),
88 BlockKind::Quote {
89 kind: Some(QuoteKind::Important),
90 text: text(),
91 },
92 ),
93 (
94 "Quote (Warning)".into(),
95 BlockKind::Quote {
96 kind: Some(QuoteKind::Warning),
97 text: text(),
98 },
99 ),
100 (
101 "Quote (Caution)".into(),
102 BlockKind::Quote {
103 kind: Some(QuoteKind::Caution),
104 text: text(),
105 },
106 ),
107 (
108 "Code".into(),
109 BlockKind::Code {
110 language: None,
111 code: text(),
112 height: None,
113 },
114 ),
115 (
116 "Table".into(),
117 BlockKind::Table {
118 align: vec![Align::Left; 2],
119 header: vec![text(), text()],
120 rows: vec![vec![text(), text()]],
121 },
122 ),
123 (
124 "Image".into(),
125 BlockKind::Image {
126 url: String::new(),
127 alt: text(),
128 width: None,
129 },
130 ),
131 ("Divider".into(), BlockKind::Rule),
132 ]
133}
134
135pub type SlashRun = Rc<dyn Fn(SlashAt, &mut Window, &mut App)>;
137
138#[derive(Clone)]
140pub enum SlashAction {
141 Block(BlockKind),
143 Run(SlashRun),
147}
148
149#[derive(Clone)]
151pub struct SlashAt {
152 pub editor: WeakEntity<crate::Editor>,
153 pub block: usize,
154}
155
156#[derive(Clone)]
158pub struct SlashRow {
159 pub label: SharedString,
160 pub icon: Option<Icon>,
161 pub action: SlashAction,
162}
163
164#[derive(Clone)]
166pub enum SlashItem {
167 Heading(SharedString),
170 Row(SlashRow),
171 Group {
174 label: SharedString,
175 icon: Option<Icon>,
176 rows: Vec<SlashRow>,
177 },
178}
179
180pub fn defaults() -> Vec<SlashItem> {
183 let mut defaults = Vec::new();
184 for (label, kind) in items() {
185 let row = SlashRow {
186 label,
187 icon: Some(glyph_of(&kind)),
188 action: SlashAction::Block(kind.clone()),
189 };
190 if !matches!(kind, BlockKind::Quote { .. }) {
191 defaults.push(SlashItem::Row(row));
192 continue;
193 }
194 let row = match row.label.strip_prefix("Quote (") {
195 Some(rest) => SlashRow {
196 label: rest.trim_end_matches(')').to_owned().into(),
197 ..row
198 },
199 None => row,
200 };
201 match defaults.last_mut() {
202 Some(SlashItem::Group { rows, .. }) => rows.push(row),
203 _ => defaults.push(SlashItem::Group {
204 label: "Quote".into(),
205 icon: Some(glyph::TextQuote.into()),
206 rows: vec![row],
207 }),
208 }
209 }
210 defaults
211}
212
213fn glyph_of(kind: &BlockKind) -> Icon {
215 match kind {
216 BlockKind::Paragraph(_) => glyph::Pilcrow.into(),
217 BlockKind::Heading { level: 1, .. } => glyph::Heading1.into(),
218 BlockKind::Heading { level: 2, .. } => glyph::Heading2.into(),
219 BlockKind::Heading { .. } => glyph::Heading3.into(),
220 BlockKind::Bullet(_) => glyph::List.into(),
221 BlockKind::Ordered { .. } => glyph::ListOrdered.into(),
222 BlockKind::Task { .. } => glyph::ListTodo.into(),
223 BlockKind::Quote { kind, .. } => match kind {
224 None => glyph::TextQuote.into(),
225 Some(QuoteKind::Note) => glyph::Info.into(),
226 Some(QuoteKind::Tip) => glyph::Lightbulb.into(),
227 Some(QuoteKind::Important) => glyph::MessageSquareWarning.into(),
228 Some(QuoteKind::Warning) => glyph::TriangleAlert.into(),
229 Some(QuoteKind::Caution) => glyph::OctagonAlert.into(),
230 },
231 BlockKind::Code { .. } => glyph::SquareCode.into(),
232 BlockKind::Table { .. } => glyph::Table.into(),
233 BlockKind::Image { .. } => glyph::Image.into(),
234 _ => glyph::SeparatorHorizontal.into(),
235 }
236}
237
238pub(crate) struct Installed(pub Vec<SlashItem>);
240
241impl Global for Installed {}
242
243pub(crate) fn installed(cx: &App) -> Vec<SlashItem> {
245 cx.try_global::<Installed>()
246 .map_or_else(defaults, |Installed(items)| items.clone())
247}
248
249#[derive(Clone)]
253struct Entry {
254 label: SharedString,
255 short: SharedString,
256 section: Option<(usize, SharedString)>,
258 group: Option<(SharedString, Option<Icon>)>,
259 icon: Option<Icon>,
260 action: SlashAction,
261}
262
263fn entries(items: Vec<SlashItem>) -> Vec<Entry> {
265 let mut entries = Vec::new();
266 let mut section = None;
267 for (ix, item) in items.into_iter().enumerate() {
268 match item {
269 SlashItem::Heading(label) => section = Some((ix, label)),
270 SlashItem::Row(row) => entries.push(Entry {
271 label: row.label.clone(),
272 short: row.label,
273 section: section.clone(),
274 group: None,
275 icon: row.icon,
276 action: row.action,
277 }),
278 SlashItem::Group { label, icon, rows } => {
279 entries.extend(rows.into_iter().map(|row| Entry {
280 label: match row.label == label {
281 true => label.clone(),
282 false => format!("{label} ({})", row.label).into(),
283 },
284 short: row.label,
285 section: section.clone(),
286 group: Some((label.clone(), icon.clone())),
287 icon: row.icon,
288 action: row.action,
289 }))
290 }
291 }
292 }
293 entries
294}
295
296pub(crate) fn turns(items: Vec<SlashItem>) -> Vec<SlashItem> {
298 let turns = |row: &SlashRow| matches!(row.action, SlashAction::Block(_));
299 items
300 .into_iter()
301 .filter_map(|item| match item {
302 SlashItem::Row(row) => turns(&row).then_some(SlashItem::Row(row)),
303 SlashItem::Group { label, icon, rows } => {
304 let rows: Vec<SlashRow> = rows.into_iter().filter(turns).collect();
305 (!rows.is_empty()).then_some(SlashItem::Group { label, icon, rows })
306 }
307 SlashItem::Heading(_) => None,
308 })
309 .collect()
310}
311
312pub fn label(kind: &BlockKind) -> Option<SharedString> {
319 items()
320 .into_iter()
321 .find(|(_, row)| same(row, kind))
322 .map(|(label, _)| label)
323}
324
325fn same(row: &BlockKind, kind: &BlockKind) -> bool {
326 match (row, kind) {
327 (BlockKind::Heading { level: a, .. }, BlockKind::Heading { level: b, .. }) => a == b,
328 (BlockKind::Quote { kind: a, .. }, BlockKind::Quote { kind: b, .. }) => a == b,
329 (row, kind) => std::mem::discriminant(row) == std::mem::discriminant(kind),
330 }
331}
332
333enum Row {
336 Heading(SharedString),
337 Block(usize),
338 Group(SharedString, Option<Icon>, Vec<usize>),
339}
340
341pub struct Rows {
343 rows: Vec<Row>,
344 entries: Vec<Entry>,
346}
347
348impl Rows {
349 pub fn new(items: Vec<SlashItem>) -> Self {
350 let mut rows = Self {
351 rows: Vec::new(),
352 entries: entries(items),
353 };
354 rows.refilter("");
355 rows
356 }
357
358 pub fn refilter(&mut self, query: &str) {
363 let order: Vec<usize> = if query.is_empty() {
364 (0..self.entries.len()).collect()
365 } else {
366 let labels: Vec<SharedString> = self
367 .entries
368 .iter()
369 .map(|entry| entry.label.clone())
370 .collect();
371 filter_indices(query, &labels)
372 };
373 let mut rows: Vec<Row> = Vec::new();
374 let mut section = None;
375 for ix in order {
376 let entry = &self.entries[ix];
377 if query.is_empty() && entry.section.as_ref().map(|(at, _)| *at) != section {
378 section = entry.section.as_ref().map(|(at, _)| *at);
379 if let Some((_, label)) = &entry.section {
380 rows.push(Row::Heading(label.clone()));
381 }
382 }
383 let Some((group, icon)) = &entry.group else {
384 rows.push(Row::Block(ix));
385 continue;
386 };
387 match rows
388 .iter_mut()
389 .find(|row| matches!(row, Row::Group(label, ..) if label == group))
390 {
391 Some(Row::Group(_, _, held)) => held.push(ix),
392 _ => rows.push(Row::Group(group.clone(), icon.clone(), vec![ix])),
393 }
394 }
395 self.rows = rows
396 .into_iter()
397 .map(|row| match row {
398 Row::Group(_, _, held) if !query.is_empty() && held.len() == 1 => {
399 Row::Block(held[0])
400 }
401 row => row,
402 })
403 .collect();
404 }
405
406 pub fn menu(&self) -> Vec<Item> {
408 self.rows
409 .iter()
410 .map(|row| match row {
411 Row::Heading(label) => Item::Heading(label.clone()),
412 Row::Block(ix) => self.item(*ix, &self.entries[*ix].label),
413 Row::Group(label, icon, group) => {
414 let submenu = Item::submenu(
415 label.clone(),
416 group
417 .iter()
418 .map(|ix| self.item(*ix, &self.entries[*ix].short))
419 .collect(),
420 );
421 match icon.clone() {
422 Some(icon) => submenu.with_icon(icon),
423 None => submenu,
424 }
425 }
426 })
427 .collect()
428 }
429
430 fn item(&self, ix: usize, label: &SharedString) -> Item {
431 let item = Item::action(label.clone());
432 match self.entries[ix].icon.clone() {
433 Some(icon) => item.with_icon(icon),
434 None => item,
435 }
436 }
437
438 fn is_group(&self, path: &[usize]) -> bool {
441 matches!(
442 (path.first().and_then(|row| self.rows.get(*row)), path.len()),
443 (Some(Row::Group(..)), 1)
444 )
445 }
446
447 pub fn action_at(&self, path: &[usize]) -> Option<SlashAction> {
449 let ix = match (self.rows.get(*path.first()?)?, path.get(1)) {
450 (Row::Block(ix), None) => *ix,
451 (Row::Group(_, _, group), Some(row)) => *group.get(*row)?,
452 _ => return None,
453 };
454 self.entries.get(ix).map(|entry| entry.action.clone())
455 }
456}
457
458pub struct Slash {
460 pub at: Cursor,
463 rows: Rows,
464 pub cursor: menu::Cursor,
465}
466
467impl Slash {
468 pub fn open(at: Cursor, items: Vec<SlashItem>) -> Self {
469 let mut slash = Self {
470 at,
471 rows: Rows::new(items),
472 cursor: menu::Cursor::default(),
473 };
474 slash.refilter("");
475 slash
476 }
477
478 pub fn refilter(&mut self, query: &str) {
479 self.rows.refilter(query);
480 self.cursor.clear();
481 self.cursor.step(&self.menu(), 1);
482 }
483
484 pub fn menu(&self) -> Vec<Item> {
485 self.rows.menu()
486 }
487
488 pub fn step(&mut self, delta: isize) {
490 let menu = self.menu();
491 self.cursor.step(&menu, delta);
492 }
493
494 pub fn descend(&mut self) -> bool {
496 let menu = self.menu();
497 self.cursor.descend(&menu)
498 }
499
500 pub fn on_group(&self) -> bool {
502 self.cursor
503 .path()
504 .is_some_and(|path| self.rows.is_group(&path))
505 }
506
507 pub fn action_at(&self, path: &[usize]) -> Option<SlashAction> {
508 self.rows.action_at(path)
509 }
510
511 pub fn query(&self, caret: Cursor, text: &str) -> Option<String> {
514 if caret.block != self.at.block || caret.part != self.at.part {
515 return None;
516 }
517 let start = self.at.offset + 1;
518 if caret.offset < start {
519 return None;
520 }
521 let query = text.get(start..caret.offset)?;
522 (!query.contains(char::is_whitespace)).then(|| query.to_string())
524 }
525}