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 },
113 ),
114 (
115 "Table".into(),
116 BlockKind::Table {
117 align: vec![Align::Left; 2],
118 header: vec![text(), text()],
119 rows: vec![vec![text(), text()]],
120 },
121 ),
122 (
123 "Image".into(),
124 BlockKind::Image {
125 url: String::new(),
126 alt: text(),
127 width: None,
128 },
129 ),
130 ("Divider".into(), BlockKind::Rule),
131 ]
132}
133
134pub type SlashRun = Rc<dyn Fn(SlashAt, &mut Window, &mut App)>;
136
137#[derive(Clone)]
139pub enum SlashAction {
140 Block(BlockKind),
142 Run(SlashRun),
146}
147
148#[derive(Clone)]
150pub struct SlashAt {
151 pub editor: WeakEntity<crate::Editor>,
152 pub block: usize,
153}
154
155#[derive(Clone)]
157pub struct SlashRow {
158 pub label: SharedString,
159 pub icon: Option<Icon>,
160 pub action: SlashAction,
161}
162
163#[derive(Clone)]
165pub enum SlashItem {
166 Row(SlashRow),
167 Group {
170 label: SharedString,
171 icon: Option<Icon>,
172 rows: Vec<SlashRow>,
173 },
174}
175
176pub fn defaults() -> Vec<SlashItem> {
179 let mut defaults = Vec::new();
180 for (label, kind) in items() {
181 let row = SlashRow {
182 label,
183 icon: Some(glyph_of(&kind)),
184 action: SlashAction::Block(kind.clone()),
185 };
186 if !matches!(kind, BlockKind::Quote { .. }) {
187 defaults.push(SlashItem::Row(row));
188 continue;
189 }
190 let row = match row.label.strip_prefix("Quote (") {
191 Some(rest) => SlashRow {
192 label: rest.trim_end_matches(')').to_owned().into(),
193 ..row
194 },
195 None => row,
196 };
197 match defaults.last_mut() {
198 Some(SlashItem::Group { rows, .. }) => rows.push(row),
199 _ => defaults.push(SlashItem::Group {
200 label: "Quote".into(),
201 icon: Some(glyph::TextQuote.into()),
202 rows: vec![row],
203 }),
204 }
205 }
206 defaults
207}
208
209fn glyph_of(kind: &BlockKind) -> Icon {
211 match kind {
212 BlockKind::Paragraph(_) => glyph::Pilcrow.into(),
213 BlockKind::Heading { level: 1, .. } => glyph::Heading1.into(),
214 BlockKind::Heading { level: 2, .. } => glyph::Heading2.into(),
215 BlockKind::Heading { .. } => glyph::Heading3.into(),
216 BlockKind::Bullet(_) => glyph::List.into(),
217 BlockKind::Ordered { .. } => glyph::ListOrdered.into(),
218 BlockKind::Task { .. } => glyph::ListTodo.into(),
219 BlockKind::Quote { kind, .. } => match kind {
220 None => glyph::TextQuote.into(),
221 Some(QuoteKind::Note) => glyph::Info.into(),
222 Some(QuoteKind::Tip) => glyph::Lightbulb.into(),
223 Some(QuoteKind::Important) => glyph::MessageSquareWarning.into(),
224 Some(QuoteKind::Warning) => glyph::TriangleAlert.into(),
225 Some(QuoteKind::Caution) => glyph::OctagonAlert.into(),
226 },
227 BlockKind::Code { .. } => glyph::SquareCode.into(),
228 BlockKind::Table { .. } => glyph::Table.into(),
229 BlockKind::Image { .. } => glyph::Image.into(),
230 _ => glyph::SeparatorHorizontal.into(),
231 }
232}
233
234pub(crate) struct Installed(pub Vec<SlashItem>);
236
237impl Global for Installed {}
238
239pub(crate) fn installed(cx: &App) -> Vec<SlashItem> {
241 cx.try_global::<Installed>()
242 .map_or_else(defaults, |Installed(items)| items.clone())
243}
244
245#[derive(Clone)]
249struct Entry {
250 label: SharedString,
251 short: SharedString,
252 group: Option<(SharedString, Option<Icon>)>,
253 icon: Option<Icon>,
254 action: SlashAction,
255}
256
257fn entries(items: Vec<SlashItem>) -> Vec<Entry> {
259 let mut entries = Vec::new();
260 for item in items {
261 match item {
262 SlashItem::Row(row) => entries.push(Entry {
263 label: row.label.clone(),
264 short: row.label,
265 group: None,
266 icon: row.icon,
267 action: row.action,
268 }),
269 SlashItem::Group { label, icon, rows } => {
270 entries.extend(rows.into_iter().map(|row| Entry {
271 label: match row.label == label {
272 true => label.clone(),
273 false => format!("{label} ({})", row.label).into(),
274 },
275 short: row.label,
276 group: Some((label.clone(), icon.clone())),
277 icon: row.icon,
278 action: row.action,
279 }))
280 }
281 }
282 }
283 entries
284}
285
286pub(crate) fn turns(items: Vec<SlashItem>) -> Vec<SlashRow> {
289 entries(items)
290 .into_iter()
291 .filter(|entry| matches!(entry.action, SlashAction::Block(_)))
292 .map(|entry| SlashRow {
293 label: entry.label,
294 icon: entry.icon,
295 action: entry.action,
296 })
297 .collect()
298}
299
300pub fn label(kind: &BlockKind) -> Option<SharedString> {
307 items()
308 .into_iter()
309 .find(|(_, row)| same(row, kind))
310 .map(|(label, _)| label)
311}
312
313fn same(row: &BlockKind, kind: &BlockKind) -> bool {
314 match (row, kind) {
315 (BlockKind::Heading { level: a, .. }, BlockKind::Heading { level: b, .. }) => a == b,
316 (BlockKind::Quote { kind: a, .. }, BlockKind::Quote { kind: b, .. }) => a == b,
317 (row, kind) => std::mem::discriminant(row) == std::mem::discriminant(kind),
318 }
319}
320
321enum Row {
324 Block(usize),
325 Group(SharedString, Option<Icon>, Vec<usize>),
326}
327
328pub struct Slash {
330 pub at: Cursor,
333 rows: Vec<Row>,
334 entries: Vec<Entry>,
336 pub cursor: menu::Cursor,
337}
338
339impl Slash {
340 pub fn open(at: Cursor, items: Vec<SlashItem>) -> Self {
341 let mut slash = Self {
342 at,
343 rows: Vec::new(),
344 entries: entries(items),
345 cursor: menu::Cursor::default(),
346 };
347 slash.refilter("");
348 slash
349 }
350
351 pub fn refilter(&mut self, query: &str) {
355 let order: Vec<usize> = if query.is_empty() {
356 (0..self.entries.len()).collect()
357 } else {
358 let labels: Vec<SharedString> = self
359 .entries
360 .iter()
361 .map(|entry| entry.label.clone())
362 .collect();
363 filter_indices(query, &labels)
364 };
365 let mut rows: Vec<Row> = Vec::new();
366 for ix in order {
367 let Some((group, icon)) = &self.entries[ix].group else {
368 rows.push(Row::Block(ix));
369 continue;
370 };
371 match rows
372 .iter_mut()
373 .find(|row| matches!(row, Row::Group(label, ..) if label == group))
374 {
375 Some(Row::Group(_, _, held)) => held.push(ix),
376 _ => rows.push(Row::Group(group.clone(), icon.clone(), vec![ix])),
377 }
378 }
379 self.rows = rows
380 .into_iter()
381 .map(|row| match row {
382 Row::Group(_, _, held) if !query.is_empty() && held.len() == 1 => {
383 Row::Block(held[0])
384 }
385 row => row,
386 })
387 .collect();
388 self.cursor.clear();
389 self.cursor.step(&self.menu(), 1);
390 }
391
392 pub fn menu(&self) -> Vec<Item> {
394 self.rows
395 .iter()
396 .map(|row| match row {
397 Row::Block(ix) => self.item(*ix, &self.entries[*ix].label),
398 Row::Group(label, icon, group) => {
399 let submenu = Item::submenu(
400 label.clone(),
401 group
402 .iter()
403 .map(|ix| self.item(*ix, &self.entries[*ix].short))
404 .collect(),
405 );
406 match icon.clone() {
407 Some(icon) => submenu.with_icon(icon),
408 None => submenu,
409 }
410 }
411 })
412 .collect()
413 }
414
415 fn item(&self, ix: usize, label: &SharedString) -> Item {
416 let item = Item::action(label.clone());
417 match self.entries[ix].icon.clone() {
418 Some(icon) => item.with_icon(icon),
419 None => item,
420 }
421 }
422
423 pub fn step(&mut self, delta: isize) {
425 let menu = self.menu();
426 self.cursor.step(&menu, delta);
427 }
428
429 pub fn descend(&mut self) -> bool {
431 let menu = self.menu();
432 self.cursor.descend(&menu)
433 }
434
435 pub fn on_group(&self) -> bool {
437 self.cursor.path().is_some_and(|path| {
438 matches!(
439 (self.rows.get(path[0]), path.len()),
440 (Some(Row::Group(..)), 1)
441 )
442 })
443 }
444
445 pub fn action_at(&self, path: &[usize]) -> Option<SlashAction> {
447 let ix = match (self.rows.get(*path.first()?)?, path.get(1)) {
448 (Row::Block(ix), None) => *ix,
449 (Row::Group(_, _, group), Some(row)) => *group.get(*row)?,
450 _ => return None,
451 };
452 self.entries.get(ix).map(|entry| entry.action.clone())
453 }
454
455 pub fn query(&self, caret: Cursor, text: &str) -> Option<String> {
458 if caret.block != self.at.block || caret.part != self.at.part {
459 return None;
460 }
461 let start = self.at.offset + 1;
462 if caret.offset < start {
463 return None;
464 }
465 let query = text.get(start..caret.offset)?;
466 (!query.contains(char::is_whitespace)).then(|| query.to_string())
468 }
469}