use gpui::SharedString;
use ui::{
menu::{self, Item},
popover::filter_indices,
};
use markdown::{Align, BlockKind, Cursor, QuoteKind, Text};
pub fn items() -> Vec<(SharedString, BlockKind)> {
let text = Text::default;
vec![
("Text".into(), BlockKind::Paragraph(text())),
(
"Heading 1".into(),
BlockKind::Heading {
level: 1,
text: text(),
},
),
(
"Heading 2".into(),
BlockKind::Heading {
level: 2,
text: text(),
},
),
(
"Heading 3".into(),
BlockKind::Heading {
level: 3,
text: text(),
},
),
("Bullet".into(), BlockKind::Bullet(text())),
(
"Numbered".into(),
BlockKind::Ordered {
number: 1,
text: text(),
},
),
(
"Task".into(),
BlockKind::Task {
checked: false,
text: text(),
},
),
(
"Quote".into(),
BlockKind::Quote {
kind: None,
text: text(),
},
),
(
"Quote (Note)".into(),
BlockKind::Quote {
kind: Some(QuoteKind::Note),
text: text(),
},
),
(
"Quote (Tip)".into(),
BlockKind::Quote {
kind: Some(QuoteKind::Tip),
text: text(),
},
),
(
"Quote (Important)".into(),
BlockKind::Quote {
kind: Some(QuoteKind::Important),
text: text(),
},
),
(
"Quote (Warning)".into(),
BlockKind::Quote {
kind: Some(QuoteKind::Warning),
text: text(),
},
),
(
"Quote (Caution)".into(),
BlockKind::Quote {
kind: Some(QuoteKind::Caution),
text: text(),
},
),
(
"Code".into(),
BlockKind::Code {
language: None,
code: text(),
},
),
(
"Table".into(),
BlockKind::Table {
align: vec![Align::Left; 2],
header: vec![text(), text()],
rows: vec![vec![text(), text()]],
},
),
(
"Image".into(),
BlockKind::Image {
url: String::new(),
alt: text(),
width: None,
},
),
("Divider".into(), BlockKind::Rule),
]
}
pub fn label(kind: &BlockKind) -> Option<SharedString> {
items()
.into_iter()
.find(|(_, row)| same(row, kind))
.map(|(label, _)| label)
}
fn same(row: &BlockKind, kind: &BlockKind) -> bool {
match (row, kind) {
(BlockKind::Heading { level: a, .. }, BlockKind::Heading { level: b, .. }) => a == b,
(BlockKind::Quote { kind: a, .. }, BlockKind::Quote { kind: b, .. }) => a == b,
(row, kind) => std::mem::discriminant(row) == std::mem::discriminant(kind),
}
}
enum Row {
Block(usize),
Group(SharedString, Vec<usize>),
}
pub struct Slash {
pub at: Cursor,
rows: Vec<Row>,
pub cursor: menu::Cursor,
}
impl Slash {
pub fn open(at: Cursor) -> Self {
let mut slash = Self {
at,
rows: Vec::new(),
cursor: menu::Cursor::default(),
};
slash.refilter("");
slash
}
pub fn refilter(&mut self, query: &str) {
let items = items();
self.rows = if query.is_empty() {
let mut rows = Vec::new();
let mut quotes = Vec::new();
for (ix, (_, kind)) in items.iter().enumerate() {
if !matches!(kind, BlockKind::Quote { .. }) {
rows.push(Row::Block(ix));
continue;
}
if quotes.is_empty() {
rows.push(Row::Group("Quote".into(), Vec::new()));
}
quotes.push(ix);
}
if let Some(Row::Group(_, group)) =
rows.iter_mut().find(|row| matches!(row, Row::Group(..)))
{
*group = quotes;
}
rows
} else {
let labels: Vec<SharedString> = items.into_iter().map(|(label, _)| label).collect();
filter_indices(query, &labels)
.into_iter()
.map(Row::Block)
.collect()
};
self.cursor.clear();
self.cursor.step(&self.menu(), 1);
}
pub fn menu(&self) -> Vec<Item> {
let items = items();
self.rows
.iter()
.map(|row| match row {
Row::Block(ix) => Item::action(items[*ix].0.clone()),
Row::Group(label, group) => Item::submenu(
label.clone(),
group
.iter()
.map(|ix| {
let label = &items[*ix].0;
let short = label
.strip_prefix("Quote (")
.and_then(|rest| rest.strip_suffix(')'))
.unwrap_or(label);
Item::action(short.to_string())
})
.collect(),
),
})
.collect()
}
pub fn step(&mut self, delta: isize) {
let menu = self.menu();
self.cursor.step(&menu, delta);
}
pub fn descend(&mut self) -> bool {
let menu = self.menu();
self.cursor.descend(&menu)
}
pub fn on_group(&self) -> bool {
self.cursor.path().is_some_and(|path| {
matches!(
(self.rows.get(path[0]), path.len()),
(Some(Row::Group(..)), 1)
)
})
}
pub fn choice(&self) -> Option<BlockKind> {
self.kind_at(&self.cursor.path()?)
}
pub fn kind_at(&self, path: &[usize]) -> Option<BlockKind> {
let ix = match (self.rows.get(*path.first()?)?, path.get(1)) {
(Row::Block(ix), None) => *ix,
(Row::Group(_, group), Some(row)) => *group.get(*row)?,
_ => return None,
};
items().into_iter().nth(ix).map(|(_, kind)| kind)
}
pub fn query(&self, caret: Cursor, text: &str) -> Option<String> {
if caret.block != self.at.block || caret.part != self.at.part {
return None;
}
let start = self.at.offset + 1;
if caret.offset < start {
return None;
}
let query = text.get(start..caret.offset)?;
(!query.contains(char::is_whitespace)).then(|| query.to_string())
}
}