mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-28 09:54:33 +00:00
markdown preview: Break up list items into individual blocks (#10852)
Fixes a panic related to rendering checkboxes, see #10824. Currently we are rendering a list into a single block, meaning the whole block has to be rendered when it is visible on screen. This would lead to performance problems when a single list block contained a lot of items (especially if it contained checkboxes). This PR splits up list items into separate blocks, meaning only the actual visible list items on screen get rendered, instead of the whole list. A nice side effect of the refactoring is, that you can actually click on individual list items now: https://github.com/zed-industries/zed/assets/53836821/5ef4200c-bd85-4e96-a8bf-e0c8b452f762 Release Notes: - Improved rendering performance of list elements inside the markdown preview --------- Co-authored-by: Remco <djsmits12@gmail.com>
This commit is contained in:
parent
664f779eb4
commit
9329ef1d78
6 changed files with 286 additions and 279 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5902,6 +5902,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion 1.0.5",
|
||||
"collections",
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
|
|
|
@ -17,6 +17,7 @@ test-support = []
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
|
|
|
@ -8,8 +8,7 @@ use std::{fmt::Display, ops::Range, path::PathBuf};
|
|||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum ParsedMarkdownElement {
|
||||
Heading(ParsedMarkdownHeading),
|
||||
/// An ordered or unordered list of items.
|
||||
List(ParsedMarkdownList),
|
||||
ListItem(ParsedMarkdownListItem),
|
||||
Table(ParsedMarkdownTable),
|
||||
BlockQuote(ParsedMarkdownBlockQuote),
|
||||
CodeBlock(ParsedMarkdownCodeBlock),
|
||||
|
@ -22,7 +21,7 @@ impl ParsedMarkdownElement {
|
|||
pub fn source_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Self::Heading(heading) => heading.source_range.clone(),
|
||||
Self::List(list) => list.source_range.clone(),
|
||||
Self::ListItem(list_item) => list_item.source_range.clone(),
|
||||
Self::Table(table) => table.source_range.clone(),
|
||||
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
|
||||
Self::CodeBlock(code_block) => code_block.source_range.clone(),
|
||||
|
@ -30,6 +29,10 @@ impl ParsedMarkdownElement {
|
|||
Self::HorizontalRule(range) => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_list_item(&self) -> bool {
|
||||
matches!(self, Self::ListItem(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -38,20 +41,14 @@ pub struct ParsedMarkdown {
|
|||
pub children: Vec<ParsedMarkdownElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownList {
|
||||
pub source_range: Range<usize>,
|
||||
pub children: Vec<ParsedMarkdownListItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownListItem {
|
||||
pub source_range: Range<usize>,
|
||||
/// How many indentations deep this item is.
|
||||
pub depth: u16,
|
||||
pub item_type: ParsedMarkdownListItemType,
|
||||
pub contents: Vec<Box<ParsedMarkdownElement>>,
|
||||
pub content: Vec<ParsedMarkdownElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -129,7 +126,7 @@ impl ParsedMarkdownTableRow {
|
|||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub struct ParsedMarkdownBlockQuote {
|
||||
pub source_range: Range<usize>,
|
||||
pub children: Vec<Box<ParsedMarkdownElement>>,
|
||||
pub children: Vec<ParsedMarkdownElement>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::markdown_elements::*;
|
||||
use async_recursion::async_recursion;
|
||||
use collections::FxHashMap;
|
||||
use gpui::FontWeight;
|
||||
use language::LanguageRegistry;
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
||||
|
@ -98,20 +99,22 @@ impl<'a> MarkdownParser<'a> {
|
|||
async fn parse_document(mut self) -> Self {
|
||||
while !self.eof() {
|
||||
if let Some(block) = self.parse_block().await {
|
||||
self.parsed.push(block);
|
||||
self.parsed.extend(block);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
async fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
|
||||
#[async_recursion]
|
||||
async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
match current {
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => {
|
||||
self.cursor += 1;
|
||||
let text = self.parse_text(false);
|
||||
Some(ParsedMarkdownElement::Paragraph(text))
|
||||
let text = self.parse_text(false, Some(source_range));
|
||||
Some(vec![ParsedMarkdownElement::Paragraph(text)])
|
||||
}
|
||||
Tag::Heading {
|
||||
level,
|
||||
|
@ -122,24 +125,24 @@ impl<'a> MarkdownParser<'a> {
|
|||
let level = *level;
|
||||
self.cursor += 1;
|
||||
let heading = self.parse_heading(level);
|
||||
Some(ParsedMarkdownElement::Heading(heading))
|
||||
Some(vec![ParsedMarkdownElement::Heading(heading)])
|
||||
}
|
||||
Tag::Table(alignment) => {
|
||||
let alignment = alignment.clone();
|
||||
self.cursor += 1;
|
||||
let table = self.parse_table(alignment);
|
||||
Some(ParsedMarkdownElement::Table(table))
|
||||
Some(vec![ParsedMarkdownElement::Table(table)])
|
||||
}
|
||||
Tag::List(order) => {
|
||||
let order = *order;
|
||||
self.cursor += 1;
|
||||
let list = self.parse_list(1, order).await;
|
||||
Some(ParsedMarkdownElement::List(list))
|
||||
let list = self.parse_list(order).await;
|
||||
Some(list)
|
||||
}
|
||||
Tag::BlockQuote => {
|
||||
self.cursor += 1;
|
||||
let block_quote = self.parse_block_quote().await;
|
||||
Some(ParsedMarkdownElement::BlockQuote(block_quote))
|
||||
Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
let language = match kind {
|
||||
|
@ -156,7 +159,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
self.cursor += 1;
|
||||
|
||||
let code_block = self.parse_code_block(language).await;
|
||||
Some(ParsedMarkdownElement::CodeBlock(code_block))
|
||||
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
|
||||
}
|
||||
_ => {
|
||||
self.cursor += 1;
|
||||
|
@ -166,7 +169,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
Event::Rule => {
|
||||
let source_range = source_range.clone();
|
||||
self.cursor += 1;
|
||||
Some(ParsedMarkdownElement::HorizontalRule(source_range))
|
||||
Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
|
||||
}
|
||||
_ => {
|
||||
self.cursor += 1;
|
||||
|
@ -175,9 +178,16 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText {
|
||||
let (_current, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
fn parse_text(
|
||||
&mut self,
|
||||
should_complete_on_soft_break: bool,
|
||||
source_range: Option<Range<usize>>,
|
||||
) -> ParsedMarkdownText {
|
||||
let source_range = source_range.unwrap_or_else(|| {
|
||||
self.current()
|
||||
.map(|(_, range)| range.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let mut text = String::new();
|
||||
let mut bold_depth = 0;
|
||||
|
@ -379,7 +389,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let text = self.parse_text(true);
|
||||
let text = self.parse_text(true, None);
|
||||
|
||||
// Advance past the heading end tag
|
||||
self.cursor += 1;
|
||||
|
@ -415,7 +425,8 @@ impl<'a> MarkdownParser<'a> {
|
|||
break;
|
||||
}
|
||||
|
||||
let (current, _source_range) = self.current().unwrap();
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
match current {
|
||||
Event::Start(Tag::TableHead)
|
||||
| Event::Start(Tag::TableRow)
|
||||
|
@ -424,7 +435,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
Event::Start(Tag::TableCell) => {
|
||||
self.cursor += 1;
|
||||
let cell_contents = self.parse_text(false);
|
||||
let cell_contents = self.parse_text(false, Some(source_range));
|
||||
current_row.push(cell_contents);
|
||||
}
|
||||
Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
|
||||
|
@ -465,35 +476,53 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
async fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let mut children = vec![];
|
||||
let mut inside_list_item = false;
|
||||
let mut order = order;
|
||||
let mut task_item = None;
|
||||
async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
|
||||
let (_, list_source_range) = self.previous().unwrap();
|
||||
|
||||
let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![];
|
||||
let mut items = Vec::new();
|
||||
let mut items_stack = vec![Vec::new()];
|
||||
let mut depth = 1;
|
||||
let mut task_item = None;
|
||||
let mut order = order;
|
||||
let mut order_stack = Vec::new();
|
||||
|
||||
let mut insertion_indices = FxHashMap::default();
|
||||
let mut source_ranges = FxHashMap::default();
|
||||
let mut start_item_range = list_source_range.clone();
|
||||
|
||||
while !self.eof() {
|
||||
let (current, _source_range) = self.current().unwrap();
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
match current {
|
||||
Event::Start(Tag::List(order)) => {
|
||||
let order = *order;
|
||||
self.cursor += 1;
|
||||
Event::Start(Tag::List(new_order)) => {
|
||||
if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
|
||||
insertion_indices.insert(depth, items.len());
|
||||
}
|
||||
|
||||
let inner_list = self.parse_list(depth + 1, order).await;
|
||||
let block = ParsedMarkdownElement::List(inner_list);
|
||||
current_list_items.push(Box::new(block));
|
||||
// We will use the start of the nested list as the end for the current item's range,
|
||||
// because we don't care about the hierarchy of list items
|
||||
if !source_ranges.contains_key(&depth) {
|
||||
source_ranges.insert(depth, start_item_range.start..source_range.start);
|
||||
}
|
||||
|
||||
order_stack.push(order);
|
||||
order = *new_order;
|
||||
self.cursor += 1;
|
||||
depth += 1;
|
||||
}
|
||||
Event::End(TagEnd::List(_)) => {
|
||||
order = order_stack.pop().flatten();
|
||||
self.cursor += 1;
|
||||
break;
|
||||
depth -= 1;
|
||||
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Start(Tag::Item) => {
|
||||
start_item_range = source_range.clone();
|
||||
|
||||
self.cursor += 1;
|
||||
inside_list_item = true;
|
||||
items_stack.push(Vec::new());
|
||||
|
||||
// Check for task list marker (`- [ ]` or `- [x]`)
|
||||
if let Some(event) = self.current_event() {
|
||||
|
@ -508,17 +537,21 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(event) = self.current_event() {
|
||||
if let Some((event, range)) = self.current() {
|
||||
// This is a plain list item.
|
||||
// For example `- some text` or `1. [Docs](./docs.md)`
|
||||
if MarkdownParser::is_text_like(event) {
|
||||
let text = self.parse_text(false);
|
||||
let text = self.parse_text(false, Some(range.clone()));
|
||||
let block = ParsedMarkdownElement::Paragraph(text);
|
||||
current_list_items.push(Box::new(block));
|
||||
if let Some(content) = items_stack.last_mut() {
|
||||
content.push(block);
|
||||
}
|
||||
} else {
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
current_list_items.push(Box::new(block));
|
||||
if let Some(content) = items_stack.last_mut() {
|
||||
content.extend(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -543,34 +576,55 @@ impl<'a> MarkdownParser<'a> {
|
|||
order = Some(current + 1);
|
||||
}
|
||||
|
||||
let contents = std::mem::replace(&mut current_list_items, vec![]);
|
||||
if let Some(content) = items_stack.pop() {
|
||||
let source_range = source_ranges
|
||||
.remove(&depth)
|
||||
.unwrap_or(start_item_range.clone());
|
||||
|
||||
children.push(ParsedMarkdownListItem {
|
||||
contents,
|
||||
depth,
|
||||
item_type,
|
||||
});
|
||||
// We need to remove the last character of the source range, because it includes the newline character
|
||||
let source_range = source_range.start..source_range.end - 1;
|
||||
let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
|
||||
source_range,
|
||||
content,
|
||||
depth,
|
||||
item_type,
|
||||
});
|
||||
|
||||
if let Some(index) = insertion_indices.get(&depth) {
|
||||
items.insert(*index, item);
|
||||
insertion_indices.remove(&depth);
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
inside_list_item = false;
|
||||
task_item = None;
|
||||
}
|
||||
_ => {
|
||||
if !inside_list_item {
|
||||
if depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// This can only happen if a list item starts with more then one paragraph,
|
||||
// or the list item contains blocks that should be rendered after the nested list items
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
current_list_items.push(Box::new(block));
|
||||
if let Some(items_stack) = items_stack.last_mut() {
|
||||
// If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
|
||||
if !insertion_indices.contains_key(&depth) {
|
||||
items_stack.extend(block);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise we need to insert the block after all the nested items
|
||||
// that have been parsed so far
|
||||
items.extend(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParsedMarkdownList {
|
||||
source_range,
|
||||
children,
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
#[async_recursion]
|
||||
|
@ -579,13 +633,13 @@ impl<'a> MarkdownParser<'a> {
|
|||
let source_range = source_range.clone();
|
||||
let mut nested_depth = 1;
|
||||
|
||||
let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
|
||||
let mut children: Vec<ParsedMarkdownElement> = vec![];
|
||||
|
||||
while !self.eof() {
|
||||
let block = self.parse_block().await;
|
||||
|
||||
if let Some(block) = block {
|
||||
children.push(Box::new(block));
|
||||
children.extend(block);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
@ -674,7 +728,6 @@ mod tests {
|
|||
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use ParsedMarkdownElement::*;
|
||||
use ParsedMarkdownListItemType::*;
|
||||
|
||||
async fn parse(input: &str) -> ParsedMarkdown {
|
||||
|
@ -688,9 +741,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
h1(text("Heading one", 0..14), 0..14),
|
||||
h2(text("Heading two", 14..29), 14..29),
|
||||
h3(text("Heading three", 29..46), 29..46),
|
||||
h1(text("Heading one", 2..13), 0..14),
|
||||
h2(text("Heading two", 17..28), 14..29),
|
||||
h3(text("Heading three", 33..46), 29..46),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -711,7 +764,7 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![h1(text("Zed", 0..6), 0..6), p("The editor", 6..16),]
|
||||
vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -881,14 +934,11 @@ Some other content
|
|||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Unordered, vec![p("Item 1", 0..9)]),
|
||||
list_item(1, Unordered, vec![p("Item 2", 9..18)]),
|
||||
list_item(1, Unordered, vec![p("Item 3", 18..27)]),
|
||||
],
|
||||
0..27
|
||||
),]
|
||||
vec![
|
||||
list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
|
||||
list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
|
||||
list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -904,13 +954,10 @@ Some other content
|
|||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Task(false, 2..5), vec![p("TODO", 2..5)]),
|
||||
list_item(1, Task(true, 13..16), vec![p("Checked", 13..16)]),
|
||||
],
|
||||
0..25
|
||||
),]
|
||||
vec![
|
||||
list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
|
||||
list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -927,13 +974,10 @@ Some other content
|
|||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Task(false, 2..5), vec![p("Task 1", 2..5)]),
|
||||
list_item(1, Task(true, 16..19), vec![p("Task 2", 16..19)]),
|
||||
],
|
||||
0..27
|
||||
),]
|
||||
vec![
|
||||
list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
|
||||
list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -965,84 +1009,21 @@ Some other content
|
|||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
list(
|
||||
vec![
|
||||
list_item(1, Unordered, vec![p("Item 1", 0..9)]),
|
||||
list_item(1, Unordered, vec![p("Item 2", 9..18)]),
|
||||
list_item(1, Unordered, vec![p("Item 3", 18..28)]),
|
||||
],
|
||||
0..28
|
||||
),
|
||||
list(
|
||||
vec![
|
||||
list_item(1, Ordered(1), vec![p("Hello", 28..37)]),
|
||||
list_item(
|
||||
1,
|
||||
Ordered(2),
|
||||
vec![
|
||||
p("Two", 37..56),
|
||||
list(
|
||||
vec![list_item(2, Ordered(1), vec![p("Three", 47..56)]),],
|
||||
47..56
|
||||
),
|
||||
]
|
||||
),
|
||||
list_item(1, Ordered(3), vec![p("Four", 56..64)]),
|
||||
list_item(1, Ordered(4), vec![p("Five", 64..73)]),
|
||||
],
|
||||
28..73
|
||||
),
|
||||
list(
|
||||
vec![
|
||||
list_item(
|
||||
1,
|
||||
Unordered,
|
||||
vec![
|
||||
p("First", 73..155),
|
||||
list(
|
||||
vec![
|
||||
list_item(
|
||||
2,
|
||||
Ordered(1),
|
||||
vec![
|
||||
p("Hello", 83..141),
|
||||
list(
|
||||
vec![list_item(
|
||||
3,
|
||||
Ordered(1),
|
||||
vec![
|
||||
p("Goodbyte", 97..141),
|
||||
list(
|
||||
vec![
|
||||
list_item(
|
||||
4,
|
||||
Unordered,
|
||||
vec![p("Inner", 117..125)]
|
||||
),
|
||||
list_item(
|
||||
4,
|
||||
Unordered,
|
||||
vec![p("Inner", 133..141)]
|
||||
),
|
||||
],
|
||||
117..141
|
||||
)
|
||||
]
|
||||
),],
|
||||
97..141
|
||||
)
|
||||
]
|
||||
),
|
||||
list_item(2, Ordered(2), vec![p("Goodbyte", 143..155)]),
|
||||
],
|
||||
83..155
|
||||
)
|
||||
]
|
||||
),
|
||||
list_item(1, Unordered, vec![p("Last", 155..162)]),
|
||||
],
|
||||
73..162
|
||||
),
|
||||
list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
|
||||
list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
|
||||
list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
|
||||
list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
|
||||
list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
|
||||
list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
|
||||
list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
|
||||
list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
|
||||
list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
|
||||
list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
|
||||
list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
|
||||
list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
|
||||
list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
|
||||
list_item(143..154, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
|
||||
list_item(155..161, 1, Unordered, vec![p("Last", 157..161)]),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -1053,23 +1034,49 @@ Some other content
|
|||
"\
|
||||
* This is a list item with two paragraphs.
|
||||
|
||||
This is the second paragraph in the list item.",
|
||||
This is the second paragraph in the list item.
|
||||
",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![list_item(
|
||||
1,
|
||||
Unordered,
|
||||
vec![
|
||||
p("This is a list item with two paragraphs.", 4..45),
|
||||
p("This is the second paragraph in the list item.", 50..96)
|
||||
],
|
||||
),],
|
||||
vec![list_item(
|
||||
0..96,
|
||||
),]
|
||||
1,
|
||||
Unordered,
|
||||
vec![
|
||||
p("This is a list item with two paragraphs.", 4..44),
|
||||
p("This is the second paragraph in the list item.", 50..97)
|
||||
],
|
||||
),],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nested_list_with_paragraph_inside() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
1. a
|
||||
1. b
|
||||
1. c
|
||||
|
||||
text
|
||||
|
||||
1. d
|
||||
",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![
|
||||
list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
|
||||
list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
|
||||
list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
|
||||
p("text", 32..37),
|
||||
list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1086,14 +1093,11 @@ Some other content
|
|||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![list(
|
||||
vec![
|
||||
list_item(1, Unordered, vec![p("code", 0..9)],),
|
||||
list_item(1, Unordered, vec![p("bold", 9..20)]),
|
||||
list_item(1, Unordered, vec![p("link", 20..50)],)
|
||||
],
|
||||
0..50,
|
||||
),]
|
||||
vec![
|
||||
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
|
||||
list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
|
||||
list_item(20..49, 1, Unordered, vec![p("link", 22..49)],)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1127,7 +1131,7 @@ Some other content
|
|||
parsed.children,
|
||||
vec![block_quote(
|
||||
vec![
|
||||
h1(text("Heading", 2..12), 2..12),
|
||||
h1(text("Heading", 4..11), 2..12),
|
||||
p("More text", 14..26),
|
||||
p("More text", 30..40)
|
||||
],
|
||||
|
@ -1157,7 +1161,7 @@ More text
|
|||
block_quote(
|
||||
vec![
|
||||
p("A", 2..4),
|
||||
block_quote(vec![h1(text("B", 10..14), 10..14)], 8..14),
|
||||
block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
|
||||
p("C", 18..20)
|
||||
],
|
||||
0..20
|
||||
|
@ -1279,7 +1283,7 @@ fn main() {
|
|||
) -> ParsedMarkdownElement {
|
||||
ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
|
||||
source_range,
|
||||
children: children.into_iter().map(Box::new).collect(),
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1297,26 +1301,18 @@ fn main() {
|
|||
})
|
||||
}
|
||||
|
||||
fn list(
|
||||
children: Vec<ParsedMarkdownListItem>,
|
||||
source_range: Range<usize>,
|
||||
) -> ParsedMarkdownElement {
|
||||
List(ParsedMarkdownList {
|
||||
source_range,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
fn list_item(
|
||||
source_range: Range<usize>,
|
||||
depth: u16,
|
||||
item_type: ParsedMarkdownListItemType,
|
||||
contents: Vec<ParsedMarkdownElement>,
|
||||
) -> ParsedMarkdownListItem {
|
||||
ParsedMarkdownListItem {
|
||||
content: Vec<ParsedMarkdownElement>,
|
||||
) -> ParsedMarkdownElement {
|
||||
ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
|
||||
source_range,
|
||||
item_type,
|
||||
depth,
|
||||
contents: contents.into_iter().map(Box::new).collect(),
|
||||
}
|
||||
content,
|
||||
})
|
||||
}
|
||||
|
||||
fn table(
|
||||
|
|
|
@ -15,6 +15,7 @@ use ui::prelude::*;
|
|||
use workspace::item::{Item, ItemHandle, TabContentParams};
|
||||
use workspace::{Pane, Workspace};
|
||||
|
||||
use crate::markdown_elements::ParsedMarkdownElement;
|
||||
use crate::OpenPreviewToTheSide;
|
||||
use crate::{
|
||||
markdown_elements::ParsedMarkdown,
|
||||
|
@ -180,9 +181,14 @@ impl MarkdownPreviewView {
|
|||
let block = contents.children.get(ix).unwrap();
|
||||
let rendered_block = render_markdown_block(block, &mut render_cx);
|
||||
|
||||
let should_apply_padding = Self::should_apply_padding_between(
|
||||
block,
|
||||
contents.children.get(ix + 1),
|
||||
);
|
||||
|
||||
div()
|
||||
.id(ix)
|
||||
.pb_3()
|
||||
.when(should_apply_padding, |this| this.pb_3())
|
||||
.group("markdown-block")
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
|
||||
if event.down.click_count == 2 {
|
||||
|
@ -404,7 +410,7 @@ impl MarkdownPreviewView {
|
|||
let Range { start, end } = block.source_range();
|
||||
|
||||
// Check if the cursor is between the last block and the current block
|
||||
if last_end > cursor && cursor < start {
|
||||
if last_end <= cursor && cursor < start {
|
||||
block_index = Some(i.saturating_sub(1));
|
||||
break;
|
||||
}
|
||||
|
@ -423,6 +429,13 @@ impl MarkdownPreviewView {
|
|||
|
||||
block_index.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn should_apply_padding_between(
|
||||
current_block: &ParsedMarkdownElement,
|
||||
next_block: Option<&ParsedMarkdownElement>,
|
||||
) -> bool {
|
||||
!(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false))
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MarkdownPreviewView {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use crate::markdown_elements::{
|
||||
HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
|
||||
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownList, ParsedMarkdownListItemType,
|
||||
ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
|
||||
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem,
|
||||
ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment,
|
||||
ParsedMarkdownTableRow, ParsedMarkdownText,
|
||||
};
|
||||
use gpui::{
|
||||
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
|
||||
|
@ -110,7 +111,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte
|
|||
match block {
|
||||
Paragraph(text) => render_markdown_paragraph(text, cx),
|
||||
Heading(heading) => render_markdown_heading(heading, cx),
|
||||
List(list) => render_markdown_list(list, cx),
|
||||
ListItem(list_item) => render_markdown_list_item(list_item, cx),
|
||||
Table(table) => render_markdown_table(table, cx),
|
||||
BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx),
|
||||
CodeBlock(code_block) => render_markdown_code_block(code_block, cx),
|
||||
|
@ -146,79 +147,77 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> AnyElement {
|
||||
fn render_markdown_list_item(
|
||||
parsed: &ParsedMarkdownListItem,
|
||||
cx: &mut RenderContext,
|
||||
) -> AnyElement {
|
||||
use ParsedMarkdownListItemType::*;
|
||||
|
||||
let mut items = vec![];
|
||||
for item in &parsed.children {
|
||||
let padding = rems((item.depth - 1) as f32 * 0.25);
|
||||
let padding = rems((parsed.depth - 1) as f32);
|
||||
|
||||
let bullet = match &item.item_type {
|
||||
Ordered(order) => format!("{}.", order).into_any_element(),
|
||||
Unordered => "•".into_any_element(),
|
||||
Task(checked, range) => div()
|
||||
.id(cx.next_id(range))
|
||||
.mt(px(3.))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"checkbox",
|
||||
if *checked {
|
||||
Selection::Selected
|
||||
} else {
|
||||
Selection::Unselected
|
||||
},
|
||||
)
|
||||
.when_some(
|
||||
cx.checkbox_clicked_callback.clone(),
|
||||
|this, callback| {
|
||||
this.on_click({
|
||||
let range = range.clone();
|
||||
move |selection, cx| {
|
||||
let checked = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected => false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if cx.modifiers().secondary() {
|
||||
callback(checked, range.clone(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
let bullet = match &parsed.item_type {
|
||||
Ordered(order) => format!("{}.", order).into_any_element(),
|
||||
Unordered => "•".into_any_element(),
|
||||
Task(checked, range) => div()
|
||||
.id(cx.next_id(range))
|
||||
.mt(px(3.))
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"checkbox",
|
||||
if *checked {
|
||||
Selection::Selected
|
||||
} else {
|
||||
Selection::Unselected
|
||||
},
|
||||
)
|
||||
.hover(|s| s.cursor_pointer())
|
||||
.tooltip(|cx| {
|
||||
let secondary_modifier = Keystroke {
|
||||
key: "".to_string(),
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
ime_key: None,
|
||||
};
|
||||
Tooltip::text(
|
||||
format!("{}-click to toggle the checkbox", secondary_modifier),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
};
|
||||
let bullet = div().mr_2().child(bullet);
|
||||
.when_some(
|
||||
cx.checkbox_clicked_callback.clone(),
|
||||
|this, callback| {
|
||||
this.on_click({
|
||||
let range = range.clone();
|
||||
move |selection, cx| {
|
||||
let checked = match selection {
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected => false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let contents: Vec<AnyElement> = item
|
||||
.contents
|
||||
.iter()
|
||||
.map(|c| render_markdown_block(c.as_ref(), cx))
|
||||
.collect();
|
||||
if cx.modifiers().secondary() {
|
||||
callback(checked, range.clone(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.hover(|s| s.cursor_pointer())
|
||||
.tooltip(|cx| {
|
||||
let secondary_modifier = Keystroke {
|
||||
key: "".to_string(),
|
||||
modifiers: Modifiers::secondary_key(),
|
||||
ime_key: None,
|
||||
};
|
||||
Tooltip::text(
|
||||
format!("{}-click to toggle the checkbox", secondary_modifier),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
};
|
||||
let bullet = div().mr_2().child(bullet);
|
||||
|
||||
let item = h_flex()
|
||||
.pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
|
||||
.items_start()
|
||||
.children(vec![bullet, div().children(contents).pr_4().w_full()]);
|
||||
let contents: Vec<AnyElement> = parsed
|
||||
.content
|
||||
.iter()
|
||||
.map(|c| render_markdown_block(c, cx))
|
||||
.collect();
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
let item = h_flex()
|
||||
.pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
|
||||
.items_start()
|
||||
.children(vec![bullet, div().children(contents).pr_4().w_full()]);
|
||||
|
||||
cx.with_common_p(div()).children(items).into_any()
|
||||
cx.with_common_p(item).into_any()
|
||||
}
|
||||
|
||||
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
|
||||
|
|
Loading…
Reference in a new issue