markdown preview: highlight code blocks (#9087)

![image](https://github.com/zed-industries/zed/assets/53836821/e20acd87-9680-4e1c-818d-7ae900bf0e31)

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:
Bennet Bo Fenner 2024-03-12 11:54:12 +01:00 committed by GitHub
parent e5bd9f184b
commit d362588055
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 264 additions and 126 deletions

1
Cargo.lock generated
View file

@ -5643,6 +5643,7 @@ dependencies = [
name = "markdown_preview" name = "markdown_preview"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-recursion 1.0.5",
"editor", "editor",
"gpui", "gpui",
"language", "language",

View file

@ -198,6 +198,7 @@ zed_actions = { path = "crates/zed_actions" }
anyhow = "1.0.57" anyhow = "1.0.57"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-recursion = "1.0.0"
async-tar = "0.4.2" async-tar = "0.4.2"
async-trait = "0.1" async-trait = "0.1"
bitflags = "2.4.2" bitflags = "2.4.2"

View file

@ -229,6 +229,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx) buffer.edit([(0..0, body.release_notes)], None, cx)
}); });
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
@ -240,6 +241,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
editor, editor,
workspace_handle, workspace_handle,
Some(tab_description), Some(tab_description),
language_registry,
cx, cx,
); );
workspace.add_item_to_active_pane(Box::new(view.clone()), cx); workspace.add_item_to_active_pane(Box::new(view.clone()), cx);

View file

@ -15,6 +15,7 @@ path = "src/markdown_preview.rs"
test-support = [] test-support = []
[dependencies] [dependencies]
async-recursion.workspace = true
editor.workspace = true editor.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true language.workspace = true

View file

@ -68,6 +68,7 @@ pub struct ParsedMarkdownCodeBlock {
pub source_range: Range<usize>, pub source_range: Range<usize>,
pub language: Option<String>, pub language: Option<String>,
pub contents: SharedString, pub contents: SharedString,
pub highlights: Option<Vec<(Range<usize>, HighlightId)>>,
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,16 +1,23 @@
use crate::markdown_elements::*; use crate::markdown_elements::*;
use async_recursion::async_recursion;
use gpui::FontWeight; use gpui::FontWeight;
use language::LanguageRegistry;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; 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, markdown_input: &str,
file_location_directory: Option<PathBuf>, file_location_directory: Option<PathBuf>,
language_registry: Option<Arc<LanguageRegistry>>,
) -> ParsedMarkdown { ) -> ParsedMarkdown {
let options = Options::all(); let options = Options::all();
let parser = Parser::new_ext(markdown_input, options); let parser = Parser::new_ext(markdown_input, options);
let parser = MarkdownParser::new(parser.into_offset_iter().collect(), file_location_directory); let parser = MarkdownParser::new(
let renderer = parser.parse_document(); parser.into_offset_iter().collect(),
file_location_directory,
language_registry,
);
let renderer = parser.parse_document().await;
ParsedMarkdown { ParsedMarkdown {
children: renderer.parsed, children: renderer.parsed,
} }
@ -23,16 +30,19 @@ struct MarkdownParser<'a> {
/// The blocks that we have successfully parsed so far /// The blocks that we have successfully parsed so far
parsed: Vec<ParsedMarkdownElement>, parsed: Vec<ParsedMarkdownElement>,
file_location_directory: Option<PathBuf>, file_location_directory: Option<PathBuf>,
language_registry: Option<Arc<LanguageRegistry>>,
} }
impl<'a> MarkdownParser<'a> { impl<'a> MarkdownParser<'a> {
fn new( fn new(
tokens: Vec<(Event<'a>, Range<usize>)>, tokens: Vec<(Event<'a>, Range<usize>)>,
file_location_directory: Option<PathBuf>, file_location_directory: Option<PathBuf>,
language_registry: Option<Arc<LanguageRegistry>>,
) -> Self { ) -> Self {
Self { Self {
tokens, tokens,
file_location_directory, file_location_directory,
language_registry,
cursor: 0, cursor: 0,
parsed: vec![], 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() { while !self.eof() {
if let Some(block) = self.parse_block() { if let Some(block) = self.parse_block().await {
self.parsed.push(block); self.parsed.push(block);
} }
} }
self self
} }
fn parse_block(&mut self) -> Option<ParsedMarkdownElement> { async fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
let (current, source_range) = self.current().unwrap(); let (current, source_range) = self.current().unwrap();
match current { match current {
Event::Start(tag) => match tag { Event::Start(tag) => match tag {
@ -119,12 +129,12 @@ impl<'a> MarkdownParser<'a> {
Tag::List(order) => { Tag::List(order) => {
let order = *order; let order = *order;
self.cursor += 1; self.cursor += 1;
let list = self.parse_list(1, order); let list = self.parse_list(1, order).await;
Some(ParsedMarkdownElement::List(list)) Some(ParsedMarkdownElement::List(list))
} }
Tag::BlockQuote => { Tag::BlockQuote => {
self.cursor += 1; self.cursor += 1;
let block_quote = self.parse_block_quote(); let block_quote = self.parse_block_quote().await;
Some(ParsedMarkdownElement::BlockQuote(block_quote)) Some(ParsedMarkdownElement::BlockQuote(block_quote))
} }
Tag::CodeBlock(kind) => { Tag::CodeBlock(kind) => {
@ -141,7 +151,7 @@ impl<'a> MarkdownParser<'a> {
self.cursor += 1; 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)) 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 (_event, source_range) = self.previous().unwrap();
let source_range = source_range.clone(); let source_range = source_range.clone();
let mut children = vec![]; let mut children = vec![];
@ -424,7 +435,7 @@ impl<'a> MarkdownParser<'a> {
let order = *order; let order = *order;
self.cursor += 1; 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); let block = ParsedMarkdownElement::List(inner_list);
current_list_items.push(Box::new(block)); current_list_items.push(Box::new(block));
} }
@ -455,7 +466,7 @@ impl<'a> MarkdownParser<'a> {
let block = ParsedMarkdownElement::Paragraph(text); let block = ParsedMarkdownElement::Paragraph(text);
current_list_items.push(Box::new(block)); current_list_items.push(Box::new(block));
} else { } else {
let block = self.parse_block(); let block = self.parse_block().await;
if let Some(block) = block { if let Some(block) = block {
current_list_items.push(Box::new(block)); current_list_items.push(Box::new(block));
} }
@ -493,7 +504,7 @@ impl<'a> MarkdownParser<'a> {
break; break;
} }
let block = self.parse_block(); let block = self.parse_block().await;
if let Some(block) = block { if let Some(block) = block {
current_list_items.push(Box::new(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 (_event, source_range) = self.previous().unwrap();
let source_range = source_range.clone(); let source_range = source_range.clone();
let mut nested_depth = 1; let mut nested_depth = 1;
@ -515,7 +527,7 @@ impl<'a> MarkdownParser<'a> {
let mut children: Vec<Box<ParsedMarkdownElement>> = vec![]; let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
while !self.eof() { while !self.eof() {
let block = self.parse_block(); let block = self.parse_block().await;
if let Some(block) = block { if let Some(block) = block {
children.push(Box::new(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 (_event, source_range) = self.previous().unwrap();
let source_range = source_range.clone(); let source_range = source_range.clone();
let mut code = String::new(); 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 { ParsedMarkdownCodeBlock {
source_range, source_range,
contents: code.trim().to_string().into(), contents: code.trim().to_string().into(),
language, language,
highlights,
} }
} }
} }
@ -587,18 +615,20 @@ impl<'a> MarkdownParser<'a> {
mod tests { mod tests {
use super::*; use super::*;
use gpui::BackgroundExecutor;
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ParsedMarkdownElement::*; use ParsedMarkdownElement::*;
use ParsedMarkdownListItemType::*; use ParsedMarkdownListItemType::*;
fn parse(input: &str) -> ParsedMarkdown { async fn parse(input: &str) -> ParsedMarkdown {
parse_markdown(input, None) parse_markdown(input, None, None).await
} }
#[test] #[gpui::test]
fn test_headings() { async fn test_headings() {
let parsed = parse("# Heading one\n## Heading two\n### Heading three"); let parsed = parse("# Heading one\n## Heading two\n### Heading three").await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -610,9 +640,9 @@ mod tests {
); );
} }
#[test] #[gpui::test]
fn test_newlines_dont_new_paragraphs() { async fn test_newlines_dont_new_paragraphs() {
let parsed = parse("Some text **that is bolded**\n and *italicized*"); let parsed = parse("Some text **that is bolded**\n and *italicized*").await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -620,9 +650,9 @@ mod tests {
); );
} }
#[test] #[gpui::test]
fn test_heading_with_paragraph() { async fn test_heading_with_paragraph() {
let parsed = parse("# Zed\nThe editor"); let parsed = parse("# Zed\nThe editor").await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -630,9 +660,9 @@ mod tests {
); );
} }
#[test] #[gpui::test]
fn test_double_newlines_do_new_paragraphs() { async fn test_double_newlines_do_new_paragraphs() {
let parsed = parse("Some text **that is bolded**\n\n and *italicized*"); let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -643,9 +673,9 @@ mod tests {
); );
} }
#[test] #[gpui::test]
fn test_bold_italic_text() { async fn test_bold_italic_text() {
let parsed = parse("Some text **that is bolded** and *italicized*"); let parsed = parse("Some text **that is bolded** and *italicized*").await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -653,9 +683,9 @@ mod tests {
); );
} }
#[test] #[gpui::test]
fn test_nested_bold_strikethrough_text() { async fn test_nested_bold_strikethrough_text() {
let parsed = parse("Some **bo~~strikethrough~~ld** text"); let parsed = parse("Some **bo~~strikethrough~~ld** text").await;
assert_eq!(parsed.children.len(), 1); assert_eq!(parsed.children.len(), 1);
assert_eq!( assert_eq!(
@ -703,8 +733,8 @@ mod tests {
); );
} }
#[test] #[gpui::test]
fn test_header_only_table() { async fn test_header_only_table() {
let markdown = "\ let markdown = "\
| Header 1 | Header 2 | | Header 1 | Header 2 |
|----------|----------| |----------|----------|
@ -719,13 +749,13 @@ Some other content
); );
assert_eq!( assert_eq!(
parse(markdown).children[0], parse(markdown).await.children[0],
ParsedMarkdownElement::Table(expected_table) ParsedMarkdownElement::Table(expected_table)
); );
} }
#[test] #[gpui::test]
fn test_basic_table() { async fn test_basic_table() {
let markdown = "\ let markdown = "\
| Header 1 | Header 2 | | Header 1 | Header 2 |
|----------|----------| |----------|----------|
@ -742,20 +772,21 @@ Some other content
); );
assert_eq!( assert_eq!(
parse(markdown).children[0], parse(markdown).await.children[0],
ParsedMarkdownElement::Table(expected_table) ParsedMarkdownElement::Table(expected_table)
); );
} }
#[test] #[gpui::test]
fn test_list_basic() { async fn test_list_basic() {
let parsed = parse( let parsed = parse(
"\ "\
* Item 1 * Item 1
* Item 2 * Item 2
* Item 3 * Item 3
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -770,14 +801,15 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_list_with_tasks() { async fn test_list_with_tasks() {
let parsed = parse( let parsed = parse(
"\ "\
- [ ] TODO - [ ] TODO
- [x] Checked - [x] Checked
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -791,8 +823,8 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_list_nested() { async fn test_list_nested() {
let parsed = parse( let parsed = parse(
"\ "\
* Item 1 * Item 1
@ -813,7 +845,8 @@ Some other content
2. Goodbyte 2. Goodbyte
* Last * Last
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -900,14 +933,15 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_list_with_nested_content() { async fn test_list_with_nested_content() {
let parsed = parse( let parsed = parse(
"\ "\
* This is a list item with two paragraphs. * 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!( assert_eq!(
parsed.children, parsed.children,
@ -925,15 +959,16 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_list_with_leading_text() { async fn test_list_with_leading_text() {
let parsed = parse( let parsed = parse(
"\ "\
* `code` * `code`
* **bold** * **bold**
* [link](https://example.com) * [link](https://example.com)
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -948,9 +983,9 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_simple_block_quote() { async fn test_simple_block_quote() {
let parsed = parse("> Simple block quote with **styled text**"); let parsed = parse("> Simple block quote with **styled text**").await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -961,8 +996,8 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_simple_block_quote_with_multiple_lines() { async fn test_simple_block_quote_with_multiple_lines() {
let parsed = parse( let parsed = parse(
"\ "\
> # Heading > # Heading
@ -971,7 +1006,8 @@ Some other content
> >
> More text > More text
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -986,8 +1022,8 @@ Some other content
); );
} }
#[test] #[gpui::test]
fn test_nested_block_quote() { async fn test_nested_block_quote() {
let parsed = parse( let parsed = parse(
"\ "\
> A > A
@ -998,7 +1034,8 @@ Some other content
More text More text
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
@ -1016,8 +1053,8 @@ More text
); );
} }
#[test] #[gpui::test]
fn test_code_block() { async fn test_code_block() {
let parsed = parse( let parsed = parse(
"\ "\
``` ```
@ -1026,17 +1063,28 @@ fn main() {
} }
``` ```
", ",
); )
.await;
assert_eq!( assert_eq!(
parsed.children, 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] #[gpui::test]
fn test_code_block_with_language() { async fn test_code_block_with_language(executor: BackgroundExecutor) {
let parsed = parse( 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 ```rust
fn main() { fn main() {
@ -1044,18 +1092,37 @@ fn main() {
} }
``` ```
", ",
); None,
Some(language_registry),
)
.await;
assert_eq!( assert_eq!(
parsed.children, parsed.children,
vec![code_block( vec![code_block(
Some("rust".into()), Some("rust".to_string()),
"fn main() {\n return 0;\n}", "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 { fn h1(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
ParsedMarkdownElement::Heading(ParsedMarkdownHeading { ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
source_range, source_range,
@ -1108,11 +1175,13 @@ fn main() {
language: Option<String>, language: Option<String>,
code: &str, code: &str,
source_range: Range<usize>, source_range: Range<usize>,
highlights: Option<Vec<(Range<usize>, HighlightId)>>,
) -> ParsedMarkdownElement { ) -> ParsedMarkdownElement {
ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock { ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
source_range, source_range,
language, language,
contents: code.to_string().into(), contents: code.to_string().into(),
highlights,
}) })
} }

View file

@ -1,3 +1,4 @@
use std::sync::Arc;
use std::{ops::Range, path::PathBuf}; use std::{ops::Range, path::PathBuf};
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
@ -5,6 +6,7 @@ use gpui::{
list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement, list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView, IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
}; };
use language::LanguageRegistry;
use ui::prelude::*; use ui::prelude::*;
use workspace::item::{Item, ItemHandle}; use workspace::item::{Item, ItemHandle};
use workspace::Workspace; use workspace::Workspace;
@ -19,7 +21,7 @@ use crate::{
pub struct MarkdownPreviewView { pub struct MarkdownPreviewView {
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
contents: ParsedMarkdown, contents: Option<ParsedMarkdown>,
selected_block: usize, selected_block: usize,
list_state: ListState, list_state: ListState,
tab_description: String, tab_description: String,
@ -34,10 +36,16 @@ impl MarkdownPreviewView {
} }
if let Some(editor) = workspace.active_item_as::<Editor>(cx) { if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
let language_registry = workspace.project().read(cx).languages().clone();
let workspace_handle = workspace.weak_handle(); let workspace_handle = workspace.weak_handle();
let tab_description = editor.tab_description(0, cx); let tab_description = editor.tab_description(0, cx);
let view: View<MarkdownPreviewView> = let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewView::new(editor, workspace_handle, tab_description, cx); editor,
workspace_handle,
tab_description,
language_registry,
cx,
);
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx); workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
cx.notify(); cx.notify();
} }
@ -48,55 +56,82 @@ impl MarkdownPreviewView {
active_editor: View<Editor>, active_editor: View<Editor>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
tab_description: Option<SharedString>, tab_description: Option<SharedString>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> View<Self> { ) -> View<Self> {
cx.new_view(|cx: &mut ViewContext<Self>| { cx.new_view(|cx: &mut ViewContext<Self>| {
let view = cx.view().downgrade(); let view = cx.view().downgrade();
let editor = active_editor.read(cx); let editor = active_editor.read(cx);
let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx); let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
let contents = editor.buffer().read(cx).snapshot(cx).text(); let contents = editor.buffer().read(cx).snapshot(cx).text();
let contents = parse_markdown(&contents, file_location);
cx.subscribe(&active_editor, |this, editor, event: &EditorEvent, cx| { let language_registry_copy = language_registry.clone();
match event { cx.spawn(|view, mut cx| async move {
EditorEvent::Edited => { let contents =
let editor = editor.read(cx); parse_markdown(&contents, file_location, Some(language_registry_copy)).await;
let contents = editor.buffer().read(cx).snapshot(cx).text();
let file_location =
MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
this.contents = parse_markdown(&contents, file_location);
this.list_state.reset(this.contents.children.len());
cx.notify();
// TODO: This does not work as expected. view.update(&mut cx, |view, cx| {
// The scroll request appears to be dropped let markdown_blocks_count = contents.children.len();
// after `.reset` is called. view.contents = Some(contents);
this.list_state.scroll_to_reveal_item(this.selected_block); view.list_state.reset(markdown_blocks_count);
cx.notify(); cx.notify();
} })
EditorEvent::SelectionsChanged { .. } => {
let editor = editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
this.selected_block = this.get_block_index_under_cursor(selection_range);
this.list_state.scroll_to_reveal_item(this.selected_block);
cx.notify();
}
_ => {}
};
}) })
.detach(); .detach();
let list_state = ListState::new( cx.subscribe(
contents.children.len(), &active_editor,
gpui::ListAlignment::Top, move |this, editor, event: &EditorEvent, cx| {
px(1000.), match event {
move |ix, cx| { EditorEvent::Edited => {
let editor = editor.read(cx);
let contents = editor.buffer().read(cx).snapshot(cx).text();
let file_location =
MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
let language_registry = language_registry.clone();
cx.spawn(move |view, mut cx| async move {
let contents = parse_markdown(
&contents,
file_location,
Some(language_registry.clone()),
)
.await;
view.update(&mut cx, move |view, cx| {
let markdown_blocks_count = contents.children.len();
view.contents = Some(contents);
let scroll_top = view.list_state.logical_scroll_top();
view.list_state.reset(markdown_blocks_count);
view.list_state.scroll_to(scroll_top);
cx.notify();
})
})
.detach();
}
EditorEvent::SelectionsChanged { .. } => {
let editor = editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
this.selected_block =
this.get_block_index_under_cursor(selection_range);
this.list_state.scroll_to_reveal_item(this.selected_block);
cx.notify();
}
_ => {}
};
},
)
.detach();
let list_state =
ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
if let Some(view) = view.upgrade() { if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
let Some(contents) = &view.contents else {
return div().into_any();
};
let mut render_cx = let mut render_cx =
RenderContext::new(Some(view.workspace.clone()), cx); RenderContext::new(Some(view.workspace.clone()), cx);
let block = view.contents.children.get(ix).unwrap(); let block = contents.children.get(ix).unwrap();
let block = render_markdown_block(block, &mut render_cx); let block = render_markdown_block(block, &mut render_cx);
let block = div().child(block).pl_4().pb_3(); let block = div().child(block).pl_4().pb_3();
@ -119,8 +154,7 @@ impl MarkdownPreviewView {
} else { } else {
div().into_any() div().into_any()
} }
}, });
);
let tab_description = tab_description let tab_description = tab_description
.map(|tab_description| format!("Preview {}", tab_description)) .map(|tab_description| format!("Preview {}", tab_description))
@ -130,9 +164,9 @@ impl MarkdownPreviewView {
selected_block: 0, selected_block: 0,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
workspace, workspace,
contents, contents: None,
list_state, list_state,
tab_description: tab_description, tab_description,
} }
}) })
} }
@ -154,18 +188,33 @@ impl MarkdownPreviewView {
} }
fn get_block_index_under_cursor(&self, selection_range: Range<usize>) -> usize { fn get_block_index_under_cursor(&self, selection_range: Range<usize>) -> usize {
let mut block_index = 0; let mut block_index = None;
let cursor = selection_range.start; let cursor = selection_range.start;
for (i, block) in self.contents.children.iter().enumerate() { let mut last_end = 0;
let Range { start, end } = block.source_range(); if let Some(content) = &self.contents {
if start <= cursor && end >= cursor { for (i, block) in content.children.iter().enumerate() {
block_index = i; let Range { start, end } = block.source_range();
break;
// Check if the cursor is between the last block and the current block
if last_end > cursor && cursor < start {
block_index = Some(i.saturating_sub(1));
break;
}
if start <= cursor && end >= cursor {
block_index = Some(i);
break;
}
last_end = end;
}
if block_index.is_none() && last_end < cursor {
block_index = Some(content.children.len().saturating_sub(1));
} }
} }
return block_index; block_index.unwrap_or_default()
} }
} }

View file

@ -248,11 +248,25 @@ fn render_markdown_code_block(
parsed: &ParsedMarkdownCodeBlock, parsed: &ParsedMarkdownCodeBlock,
cx: &mut RenderContext, cx: &mut RenderContext,
) -> AnyElement { ) -> AnyElement {
let body = if let Some(highlights) = parsed.highlights.as_ref() {
StyledText::new(parsed.contents.clone()).with_highlights(
&cx.text_style,
highlights.iter().filter_map(|(range, highlight_id)| {
highlight_id
.style(cx.syntax_theme.as_ref())
.map(|style| (range.clone(), style))
}),
)
} else {
StyledText::new(parsed.contents.clone())
};
cx.with_common_p(div()) cx.with_common_p(div())
.px_3() .px_3()
.py_3() .py_3()
.bg(cx.code_block_background_color) .bg(cx.code_block_background_color)
.child(StyledText::new(parsed.contents.clone())) .rounded_md()
.child(body)
.into_any() .into_any()
} }

View file

@ -25,7 +25,7 @@ test-support = [
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-recursion = "1.0.0" async-recursion.workspace = true
bincode = "1.2.1" bincode = "1.2.1"
call.workspace = true call.workspace = true
client.workspace = true client.workspace = true