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),
145}
146
147#[derive(Clone)]
149pub struct SlashAt {
150 pub editor: WeakEntity<crate::Editor>,
151 pub block: usize,
152}
153
154#[derive(Clone)]
156pub struct SlashRow {
157 pub label: SharedString,
158 pub icon: Option<Icon>,
159 pub action: SlashAction,
160}
161
162#[derive(Clone)]
164pub enum SlashItem {
165 Row(SlashRow),
166 Group {
169 label: SharedString,
170 icon: Option<Icon>,
171 rows: Vec<SlashRow>,
172 },
173}
174
175pub fn defaults() -> Vec<SlashItem> {
178 let mut defaults = Vec::new();
179 for (label, kind) in items() {
180 let row = SlashRow {
181 label,
182 icon: Some(glyph_of(&kind)),
183 action: SlashAction::Block(kind.clone()),
184 };
185 if !matches!(kind, BlockKind::Quote { .. }) {
186 defaults.push(SlashItem::Row(row));
187 continue;
188 }
189 let row = match row.label.strip_prefix("Quote (") {
190 Some(rest) => SlashRow {
191 label: rest.trim_end_matches(')').to_owned().into(),
192 ..row
193 },
194 None => row,
195 };
196 match defaults.last_mut() {
197 Some(SlashItem::Group { rows, .. }) => rows.push(row),
198 _ => defaults.push(SlashItem::Group {
199 label: "Quote".into(),
200 icon: Some(glyph::TextQuote.into()),
201 rows: vec![row],
202 }),
203 }
204 }
205 defaults
206}
207
208fn glyph_of(kind: &BlockKind) -> Icon {
210 match kind {
211 BlockKind::Paragraph(_) => glyph::Pilcrow.into(),
212 BlockKind::Heading { level: 1, .. } => glyph::Heading1.into(),
213 BlockKind::Heading { level: 2, .. } => glyph::Heading2.into(),
214 BlockKind::Heading { .. } => glyph::Heading3.into(),
215 BlockKind::Bullet(_) => glyph::List.into(),
216 BlockKind::Ordered { .. } => glyph::ListOrdered.into(),
217 BlockKind::Task { .. } => glyph::ListTodo.into(),
218 BlockKind::Quote { kind, .. } => match kind {
219 None => glyph::TextQuote.into(),
220 Some(QuoteKind::Note) => glyph::Info.into(),
221 Some(QuoteKind::Tip) => glyph::Lightbulb.into(),
222 Some(QuoteKind::Important) => glyph::MessageSquareWarning.into(),
223 Some(QuoteKind::Warning) => glyph::TriangleAlert.into(),
224 Some(QuoteKind::Caution) => glyph::OctagonAlert.into(),
225 },
226 BlockKind::Code { .. } => glyph::SquareCode.into(),
227 BlockKind::Table { .. } => glyph::Table.into(),
228 BlockKind::Image { .. } => glyph::Image.into(),
229 _ => glyph::SeparatorHorizontal.into(),
230 }
231}
232
233pub(crate) struct Installed(pub Vec<SlashItem>);
235
236impl Global for Installed {}
237
238pub(crate) fn installed(cx: &App) -> Vec<SlashItem> {
240 cx.try_global::<Installed>()
241 .map_or_else(defaults, |Installed(items)| items.clone())
242}
243
244#[derive(Clone)]
248struct Entry {
249 label: SharedString,
250 short: SharedString,
251 group: Option<(SharedString, Option<Icon>)>,
252 icon: Option<Icon>,
253 action: SlashAction,
254}
255
256fn entries(items: Vec<SlashItem>) -> Vec<Entry> {
258 let mut entries = Vec::new();
259 for item in items {
260 match item {
261 SlashItem::Row(row) => entries.push(Entry {
262 label: row.label.clone(),
263 short: row.label,
264 group: None,
265 icon: row.icon,
266 action: row.action,
267 }),
268 SlashItem::Group { label, icon, rows } => {
269 entries.extend(rows.into_iter().map(|row| Entry {
270 label: match row.label == label {
271 true => label.clone(),
272 false => format!("{label} ({})", row.label).into(),
273 },
274 short: row.label,
275 group: Some((label.clone(), icon.clone())),
276 icon: row.icon,
277 action: row.action,
278 }))
279 }
280 }
281 }
282 entries
283}
284
285pub fn label(kind: &BlockKind) -> Option<SharedString> {
292 items()
293 .into_iter()
294 .find(|(_, row)| same(row, kind))
295 .map(|(label, _)| label)
296}
297
298fn same(row: &BlockKind, kind: &BlockKind) -> bool {
299 match (row, kind) {
300 (BlockKind::Heading { level: a, .. }, BlockKind::Heading { level: b, .. }) => a == b,
301 (BlockKind::Quote { kind: a, .. }, BlockKind::Quote { kind: b, .. }) => a == b,
302 (row, kind) => std::mem::discriminant(row) == std::mem::discriminant(kind),
303 }
304}
305
306enum Row {
309 Block(usize),
310 Group(SharedString, Option<Icon>, Vec<usize>),
311}
312
313pub struct Slash {
315 pub at: Cursor,
318 rows: Vec<Row>,
319 entries: Vec<Entry>,
321 pub cursor: menu::Cursor,
322}
323
324impl Slash {
325 pub fn open(at: Cursor, items: Vec<SlashItem>) -> Self {
326 let mut slash = Self {
327 at,
328 rows: Vec::new(),
329 entries: entries(items),
330 cursor: menu::Cursor::default(),
331 };
332 slash.refilter("");
333 slash
334 }
335
336 pub fn refilter(&mut self, query: &str) {
340 let order: Vec<usize> = if query.is_empty() {
341 (0..self.entries.len()).collect()
342 } else {
343 let labels: Vec<SharedString> = self
344 .entries
345 .iter()
346 .map(|entry| entry.label.clone())
347 .collect();
348 filter_indices(query, &labels)
349 };
350 let mut rows: Vec<Row> = Vec::new();
351 for ix in order {
352 let Some((group, icon)) = &self.entries[ix].group else {
353 rows.push(Row::Block(ix));
354 continue;
355 };
356 match rows
357 .iter_mut()
358 .find(|row| matches!(row, Row::Group(label, ..) if label == group))
359 {
360 Some(Row::Group(_, _, held)) => held.push(ix),
361 _ => rows.push(Row::Group(group.clone(), icon.clone(), vec![ix])),
362 }
363 }
364 self.rows = rows
365 .into_iter()
366 .map(|row| match row {
367 Row::Group(_, _, held) if !query.is_empty() && held.len() == 1 => {
368 Row::Block(held[0])
369 }
370 row => row,
371 })
372 .collect();
373 self.cursor.clear();
374 self.cursor.step(&self.menu(), 1);
375 }
376
377 pub fn menu(&self) -> Vec<Item> {
379 self.rows
380 .iter()
381 .map(|row| match row {
382 Row::Block(ix) => self.item(*ix, &self.entries[*ix].label),
383 Row::Group(label, icon, group) => {
384 let submenu = Item::submenu(
385 label.clone(),
386 group
387 .iter()
388 .map(|ix| self.item(*ix, &self.entries[*ix].short))
389 .collect(),
390 );
391 match icon.clone() {
392 Some(icon) => submenu.with_icon(icon),
393 None => submenu,
394 }
395 }
396 })
397 .collect()
398 }
399
400 fn item(&self, ix: usize, label: &SharedString) -> Item {
401 let item = Item::action(label.clone());
402 match self.entries[ix].icon.clone() {
403 Some(icon) => item.with_icon(icon),
404 None => item,
405 }
406 }
407
408 pub fn step(&mut self, delta: isize) {
410 let menu = self.menu();
411 self.cursor.step(&menu, delta);
412 }
413
414 pub fn descend(&mut self) -> bool {
416 let menu = self.menu();
417 self.cursor.descend(&menu)
418 }
419
420 pub fn on_group(&self) -> bool {
422 self.cursor.path().is_some_and(|path| {
423 matches!(
424 (self.rows.get(path[0]), path.len()),
425 (Some(Row::Group(..)), 1)
426 )
427 })
428 }
429
430 pub fn action_at(&self, path: &[usize]) -> Option<SlashAction> {
432 let ix = match (self.rows.get(*path.first()?)?, path.get(1)) {
433 (Row::Block(ix), None) => *ix,
434 (Row::Group(_, _, group), Some(row)) => *group.get(*row)?,
435 _ => return None,
436 };
437 self.entries.get(ix).map(|entry| entry.action.clone())
438 }
439
440 pub fn query(&self, caret: Cursor, text: &str) -> Option<String> {
443 if caret.block != self.at.block || caret.part != self.at.part {
444 return None;
445 }
446 let start = self.at.offset + 1;
447 if caret.offset < start {
448 return None;
449 }
450 let query = text.get(start..caret.offset)?;
451 (!query.contains(char::is_whitespace)).then(|| query.to_string())
453 }
454}