markdown preview: highlight code blocks (#9087)
 Release Notes: - Added syntax highlighting to code blocks in markdown preview - Fixed scroll position in markdown preview when editing a markdown file (#9208)
This commit is contained in:
parent
e5bd9f184b
commit
d362588055
9 changed files with 264 additions and 126 deletions
|
@ -1,16 +1,23 @@
|
|||
use crate::markdown_elements::*;
|
||||
use async_recursion::async_recursion;
|
||||
use gpui::FontWeight;
|
||||
use language::LanguageRegistry;
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc};
|
||||
|
||||
pub fn parse_markdown(
|
||||
pub async fn parse_markdown(
|
||||
markdown_input: &str,
|
||||
file_location_directory: Option<PathBuf>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
) -> ParsedMarkdown {
|
||||
let options = Options::all();
|
||||
let parser = Parser::new_ext(markdown_input, options);
|
||||
let parser = MarkdownParser::new(parser.into_offset_iter().collect(), file_location_directory);
|
||||
let renderer = parser.parse_document();
|
||||
let parser = MarkdownParser::new(
|
||||
parser.into_offset_iter().collect(),
|
||||
file_location_directory,
|
||||
language_registry,
|
||||
);
|
||||
let renderer = parser.parse_document().await;
|
||||
ParsedMarkdown {
|
||||
children: renderer.parsed,
|
||||
}
|
||||
|
@ -23,16 +30,19 @@ struct MarkdownParser<'a> {
|
|||
/// The blocks that we have successfully parsed so far
|
||||
parsed: Vec<ParsedMarkdownElement>,
|
||||
file_location_directory: Option<PathBuf>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
}
|
||||
|
||||
impl<'a> MarkdownParser<'a> {
|
||||
fn new(
|
||||
tokens: Vec<(Event<'a>, Range<usize>)>,
|
||||
file_location_directory: Option<PathBuf>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tokens,
|
||||
file_location_directory,
|
||||
language_registry,
|
||||
cursor: 0,
|
||||
parsed: vec![],
|
||||
}
|
||||
|
@ -81,16 +91,16 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_document(mut self) -> Self {
|
||||
async fn parse_document(mut self) -> Self {
|
||||
while !self.eof() {
|
||||
if let Some(block) = self.parse_block() {
|
||||
if let Some(block) = self.parse_block().await {
|
||||
self.parsed.push(block);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
|
||||
async fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
|
||||
let (current, source_range) = self.current().unwrap();
|
||||
match current {
|
||||
Event::Start(tag) => match tag {
|
||||
|
@ -119,12 +129,12 @@ impl<'a> MarkdownParser<'a> {
|
|||
Tag::List(order) => {
|
||||
let order = *order;
|
||||
self.cursor += 1;
|
||||
let list = self.parse_list(1, order);
|
||||
let list = self.parse_list(1, order).await;
|
||||
Some(ParsedMarkdownElement::List(list))
|
||||
}
|
||||
Tag::BlockQuote => {
|
||||
self.cursor += 1;
|
||||
let block_quote = self.parse_block_quote();
|
||||
let block_quote = self.parse_block_quote().await;
|
||||
Some(ParsedMarkdownElement::BlockQuote(block_quote))
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
|
@ -141,7 +151,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
|
||||
self.cursor += 1;
|
||||
|
||||
let code_block = self.parse_code_block(language);
|
||||
let code_block = self.parse_code_block(language).await;
|
||||
Some(ParsedMarkdownElement::CodeBlock(code_block))
|
||||
}
|
||||
_ => {
|
||||
|
@ -407,7 +417,8 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList {
|
||||
#[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![];
|
||||
|
@ -424,7 +435,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
let order = *order;
|
||||
self.cursor += 1;
|
||||
|
||||
let inner_list = self.parse_list(depth + 1, order);
|
||||
let inner_list = self.parse_list(depth + 1, order).await;
|
||||
let block = ParsedMarkdownElement::List(inner_list);
|
||||
current_list_items.push(Box::new(block));
|
||||
}
|
||||
|
@ -455,7 +466,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
let block = ParsedMarkdownElement::Paragraph(text);
|
||||
current_list_items.push(Box::new(block));
|
||||
} else {
|
||||
let block = self.parse_block();
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
current_list_items.push(Box::new(block));
|
||||
}
|
||||
|
@ -493,7 +504,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
break;
|
||||
}
|
||||
|
||||
let block = self.parse_block();
|
||||
let block = self.parse_block().await;
|
||||
if let Some(block) = block {
|
||||
current_list_items.push(Box::new(block));
|
||||
}
|
||||
|
@ -507,7 +518,8 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
|
||||
#[async_recursion]
|
||||
async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let mut nested_depth = 1;
|
||||
|
@ -515,7 +527,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
|
||||
|
||||
while !self.eof() {
|
||||
let block = self.parse_block();
|
||||
let block = self.parse_block().await;
|
||||
|
||||
if let Some(block) = block {
|
||||
children.push(Box::new(block));
|
||||
|
@ -553,7 +565,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_code_block(&mut self, language: Option<String>) -> ParsedMarkdownCodeBlock {
|
||||
async fn parse_code_block(&mut self, language: Option<String>) -> ParsedMarkdownCodeBlock {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let mut code = String::new();
|
||||
|
@ -575,10 +587,26 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let highlights = if let Some(language) = &language {
|
||||
if let Some(registry) = &self.language_registry {
|
||||
let rope: language::Rope = code.as_str().into();
|
||||
registry
|
||||
.language_for_name_or_extension(language)
|
||||
.await
|
||||
.map(|l| l.highlight_text(&rope, 0..code.len()))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ParsedMarkdownCodeBlock {
|
||||
source_range,
|
||||
contents: code.trim().to_string().into(),
|
||||
language,
|
||||
highlights,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -587,18 +615,20 @@ impl<'a> MarkdownParser<'a> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use gpui::BackgroundExecutor;
|
||||
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use ParsedMarkdownElement::*;
|
||||
use ParsedMarkdownListItemType::*;
|
||||
|
||||
fn parse(input: &str) -> ParsedMarkdown {
|
||||
parse_markdown(input, None)
|
||||
async fn parse(input: &str) -> ParsedMarkdown {
|
||||
parse_markdown(input, None, None).await
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headings() {
|
||||
let parsed = parse("# Heading one\n## Heading two\n### Heading three");
|
||||
#[gpui::test]
|
||||
async fn test_headings() {
|
||||
let parsed = parse("# Heading one\n## Heading two\n### Heading three").await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -610,9 +640,9 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_newlines_dont_new_paragraphs() {
|
||||
let parsed = parse("Some text **that is bolded**\n and *italicized*");
|
||||
#[gpui::test]
|
||||
async fn test_newlines_dont_new_paragraphs() {
|
||||
let parsed = parse("Some text **that is bolded**\n and *italicized*").await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -620,9 +650,9 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heading_with_paragraph() {
|
||||
let parsed = parse("# Zed\nThe editor");
|
||||
#[gpui::test]
|
||||
async fn test_heading_with_paragraph() {
|
||||
let parsed = parse("# Zed\nThe editor").await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -630,9 +660,9 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_double_newlines_do_new_paragraphs() {
|
||||
let parsed = parse("Some text **that is bolded**\n\n and *italicized*");
|
||||
#[gpui::test]
|
||||
async fn test_double_newlines_do_new_paragraphs() {
|
||||
let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -643,9 +673,9 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bold_italic_text() {
|
||||
let parsed = parse("Some text **that is bolded** and *italicized*");
|
||||
#[gpui::test]
|
||||
async fn test_bold_italic_text() {
|
||||
let parsed = parse("Some text **that is bolded** and *italicized*").await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -653,9 +683,9 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_bold_strikethrough_text() {
|
||||
let parsed = parse("Some **bo~~strikethrough~~ld** text");
|
||||
#[gpui::test]
|
||||
async fn test_nested_bold_strikethrough_text() {
|
||||
let parsed = parse("Some **bo~~strikethrough~~ld** text").await;
|
||||
|
||||
assert_eq!(parsed.children.len(), 1);
|
||||
assert_eq!(
|
||||
|
@ -703,8 +733,8 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_header_only_table() {
|
||||
#[gpui::test]
|
||||
async fn test_header_only_table() {
|
||||
let markdown = "\
|
||||
| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
|
@ -719,13 +749,13 @@ Some other content
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
parse(markdown).children[0],
|
||||
parse(markdown).await.children[0],
|
||||
ParsedMarkdownElement::Table(expected_table)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_table() {
|
||||
#[gpui::test]
|
||||
async fn test_basic_table() {
|
||||
let markdown = "\
|
||||
| Header 1 | Header 2 |
|
||||
|----------|----------|
|
||||
|
@ -742,20 +772,21 @@ Some other content
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
parse(markdown).children[0],
|
||||
parse(markdown).await.children[0],
|
||||
ParsedMarkdownElement::Table(expected_table)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_basic() {
|
||||
#[gpui::test]
|
||||
async fn test_list_basic() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
* Item 1
|
||||
* Item 2
|
||||
* Item 3
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -770,14 +801,15 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_with_tasks() {
|
||||
#[gpui::test]
|
||||
async fn test_list_with_tasks() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
- [ ] TODO
|
||||
- [x] Checked
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -791,8 +823,8 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_nested() {
|
||||
#[gpui::test]
|
||||
async fn test_list_nested() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
* Item 1
|
||||
|
@ -813,7 +845,8 @@ Some other content
|
|||
2. Goodbyte
|
||||
* Last
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -900,14 +933,15 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_with_nested_content() {
|
||||
#[gpui::test]
|
||||
async fn test_list_with_nested_content() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
* This is a list item with two paragraphs.
|
||||
|
||||
This is the second paragraph in the list item.",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -925,15 +959,16 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_with_leading_text() {
|
||||
#[gpui::test]
|
||||
async fn test_list_with_leading_text() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
* `code`
|
||||
* **bold**
|
||||
* [link](https://example.com)
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -948,9 +983,9 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_block_quote() {
|
||||
let parsed = parse("> Simple block quote with **styled text**");
|
||||
#[gpui::test]
|
||||
async fn test_simple_block_quote() {
|
||||
let parsed = parse("> Simple block quote with **styled text**").await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -961,8 +996,8 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_block_quote_with_multiple_lines() {
|
||||
#[gpui::test]
|
||||
async fn test_simple_block_quote_with_multiple_lines() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
> # Heading
|
||||
|
@ -971,7 +1006,8 @@ Some other content
|
|||
>
|
||||
> More text
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -986,8 +1022,8 @@ Some other content
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_block_quote() {
|
||||
#[gpui::test]
|
||||
async fn test_nested_block_quote() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
> A
|
||||
|
@ -998,7 +1034,8 @@ Some other content
|
|||
|
||||
More text
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
|
@ -1016,8 +1053,8 @@ More text
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_block() {
|
||||
#[gpui::test]
|
||||
async fn test_code_block() {
|
||||
let parsed = parse(
|
||||
"\
|
||||
```
|
||||
|
@ -1026,17 +1063,28 @@ fn main() {
|
|||
}
|
||||
```
|
||||
",
|
||||
);
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![code_block(None, "fn main() {\n return 0;\n}", 0..35)]
|
||||
vec![code_block(
|
||||
None,
|
||||
"fn main() {\n return 0;\n}",
|
||||
0..35,
|
||||
None
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_block_with_language() {
|
||||
let parsed = parse(
|
||||
#[gpui::test]
|
||||
async fn test_code_block_with_language(executor: BackgroundExecutor) {
|
||||
let mut language_registry = LanguageRegistry::test();
|
||||
language_registry.set_executor(executor);
|
||||
let language_registry = Arc::new(language_registry);
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let parsed = parse_markdown(
|
||||
"\
|
||||
```rust
|
||||
fn main() {
|
||||
|
@ -1044,18 +1092,37 @@ fn main() {
|
|||
}
|
||||
```
|
||||
",
|
||||
);
|
||||
None,
|
||||
Some(language_registry),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
parsed.children,
|
||||
vec![code_block(
|
||||
Some("rust".into()),
|
||||
Some("rust".to_string()),
|
||||
"fn main() {\n return 0;\n}",
|
||||
0..39
|
||||
0..39,
|
||||
Some(vec![])
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
fn rust_lang() -> Arc<Language> {
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".into()],
|
||||
..Default::default()
|
||||
},
|
||||
collapsed_placeholder: " /* ... */ ".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
))
|
||||
}
|
||||
|
||||
fn h1(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
|
||||
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
|
||||
source_range,
|
||||
|
@ -1108,11 +1175,13 @@ fn main() {
|
|||
language: Option<String>,
|
||||
code: &str,
|
||||
source_range: Range<usize>,
|
||||
highlights: Option<Vec<(Range<usize>, HighlightId)>>,
|
||||
) -> ParsedMarkdownElement {
|
||||
ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
|
||||
source_range,
|
||||
language,
|
||||
contents: code.to_string().into(),
|
||||
highlights,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue