markdown_preview: Improved markdown rendering support (#7345)

Kieran Gill created

This PR improves support for rendering markdown documents.

## After the updates


https://github.com/zed-industries/zed/assets/18583882/48315901-563d-44c6-8265-8390e8eed942

## Before the updates


https://github.com/zed-industries/zed/assets/18583882/6d7ddb55-41f7-492e-af12-6ab54559f612

## New features

- @SomeoneToIgnore's [scrolling feature
request](https://github.com/zed-industries/zed/pull/6958#pullrequestreview-1850458632).
- Checkboxes (`- [ ]` and `- [x]`)
- Inline code blocks.
- Ordered and unordered lists at an arbitrary depth.
- Block quotes that render nested content, like code blocks.
- Lists that render nested content, like code blocks.
- Block quotes that support variable heading sizes and the other
markdown features added
[here](https://github.com/zed-industries/zed/pull/6958).
- Users can see and click internal links (`[See the docs](./docs.md)`).

## Notable changes

- Removed dependency on `rich_text`.
- Added a new method for parsing markdown into renderable structs. This
method uses recursive descent so it can easily support more complex
markdown documents.
- Parsing does not happen for every call to
`MarkdownPreviewView::render` anymore.

## TODO

- [ ] Typing should move the markdown preview cursor.

## Future work under consideration

- If a title exists for a link, show it on hover.
- Images. 
- Since this PR brings the most support for markdown, we can consolidate
`languages/markdown` and `rich_text` to use this new renderer. Note that
the updated inline text rendering method in this PR originated from
`langauges/markdown`.
- Syntax highlighting in code blocks.
- Footnote references.
- Inline HTML.
- Strikethrough support.
- Scrolling improvements:
- Handle automatic preview scrolling when multiple cursors are used in
the editor.
- > great to see that the render now respects editor's scrolls, but can
we also support the vice-versa (as syntax tree does it in Zed) — when
scrolling the render, it would be good to scroll the editor too
- > sometimes it's hard to understand where the "caret" on the render
is, so I wonder if we could go even further with its placement and place
it inside the text, as a regular caret? Maybe even support the
selections?
- > switching to another markdown tab does not change the rendered
contents and when I call the render command again, the screen gets
another split — I would rather prefer to have Zed's syntax tree
behavior: there's always a single panel that renders things for whatever
tab is active now. At least we should not split if there's already a
split, rather adding the new rendered tab there.
- > plaintext URLs could get a highlight and the click action

## Release Notes

- Improved support for markdown rendering.

Change summary

Cargo.lock                                           |    2 
crates/markdown_preview/Cargo.toml                   |    2 
crates/markdown_preview/src/markdown_elements.rs     |  242 +++
crates/markdown_preview/src/markdown_parser.rs       | 1110 +++++++++++++
crates/markdown_preview/src/markdown_preview.rs      |    2 
crates/markdown_preview/src/markdown_preview_view.rs |  184 +
crates/markdown_preview/src/markdown_renderer.rs     |  578 +++---
7 files changed, 1,767 insertions(+), 353 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4527,9 +4527,9 @@ dependencies = [
  "lazy_static",
  "log",
  "menu",
+ "pretty_assertions",
  "project",
  "pulldown-cmark",
- "rich_text",
  "theme",
  "ui",
  "util",

crates/markdown_preview/Cargo.toml 🔗

@@ -20,8 +20,8 @@ lazy_static.workspace = true
 log.workspace = true
 menu.workspace = true
 project.workspace = true
+pretty_assertions.workspace = true
 pulldown-cmark.workspace = true
-rich_text.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true

crates/markdown_preview/src/markdown_elements.rs 🔗

@@ -0,0 +1,242 @@
+use gpui::{px, FontStyle, FontWeight, HighlightStyle, SharedString, UnderlineStyle};
+use language::HighlightId;
+use std::{ops::Range, path::PathBuf};
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub enum ParsedMarkdownElement {
+    Heading(ParsedMarkdownHeading),
+    /// An ordered or unordered list of items.
+    List(ParsedMarkdownList),
+    Table(ParsedMarkdownTable),
+    BlockQuote(ParsedMarkdownBlockQuote),
+    CodeBlock(ParsedMarkdownCodeBlock),
+    /// A paragraph of text and other inline elements.
+    Paragraph(ParsedMarkdownText),
+    HorizontalRule(Range<usize>),
+}
+
+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::Table(table) => table.source_range.clone(),
+            Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
+            Self::CodeBlock(code_block) => code_block.source_range.clone(),
+            Self::Paragraph(text) => text.source_range.clone(),
+            Self::HorizontalRule(range) => range.clone(),
+        }
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+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 {
+    /// How many indentations deep this item is.
+    pub depth: u16,
+    pub item_type: ParsedMarkdownListItemType,
+    pub contents: Vec<Box<ParsedMarkdownElement>>,
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub enum ParsedMarkdownListItemType {
+    Ordered(u64),
+    Task(bool),
+    Unordered,
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedMarkdownCodeBlock {
+    pub source_range: Range<usize>,
+    pub language: Option<String>,
+    pub contents: SharedString,
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedMarkdownHeading {
+    pub source_range: Range<usize>,
+    pub level: HeadingLevel,
+    pub contents: ParsedMarkdownText,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum HeadingLevel {
+    H1,
+    H2,
+    H3,
+    H4,
+    H5,
+    H6,
+}
+
+#[derive(Debug)]
+pub struct ParsedMarkdownTable {
+    pub source_range: Range<usize>,
+    pub header: ParsedMarkdownTableRow,
+    pub body: Vec<ParsedMarkdownTableRow>,
+    pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(test, derive(PartialEq))]
+pub enum ParsedMarkdownTableAlignment {
+    /// Default text alignment.
+    None,
+    Left,
+    Center,
+    Right,
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedMarkdownTableRow {
+    pub children: Vec<ParsedMarkdownText>,
+}
+
+impl ParsedMarkdownTableRow {
+    pub fn new() -> Self {
+        Self {
+            children: Vec::new(),
+        }
+    }
+
+    pub fn with_children(children: Vec<ParsedMarkdownText>) -> Self {
+        Self { children }
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedMarkdownBlockQuote {
+    pub source_range: Range<usize>,
+    pub children: Vec<Box<ParsedMarkdownElement>>,
+}
+
+#[derive(Debug)]
+pub struct ParsedMarkdownText {
+    /// Where the text is located in the source Markdown document.
+    pub source_range: Range<usize>,
+    /// The text content stripped of any formatting symbols.
+    pub contents: String,
+    /// The list of highlights contained in the Markdown document.
+    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
+    /// The regions of the various ranges in the Markdown document.
+    pub region_ranges: Vec<Range<usize>>,
+    /// The regions of the Markdown document.
+    pub regions: Vec<ParsedRegion>,
+}
+
+/// A run of highlighted Markdown text.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MarkdownHighlight {
+    /// A styled Markdown highlight.
+    Style(MarkdownHighlightStyle),
+    /// A highlighted code block.
+    Code(HighlightId),
+}
+
+impl MarkdownHighlight {
+    /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
+    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
+        match self {
+            MarkdownHighlight::Style(style) => {
+                let mut highlight = HighlightStyle::default();
+
+                if style.italic {
+                    highlight.font_style = Some(FontStyle::Italic);
+                }
+
+                if style.underline {
+                    highlight.underline = Some(UnderlineStyle {
+                        thickness: px(1.),
+                        ..Default::default()
+                    });
+                }
+
+                if style.weight != FontWeight::default() {
+                    highlight.font_weight = Some(style.weight);
+                }
+
+                Some(highlight)
+            }
+
+            MarkdownHighlight::Code(id) => id.style(theme),
+        }
+    }
+}
+
+/// The style for a Markdown highlight.
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct MarkdownHighlightStyle {
+    /// Whether the text should be italicized.
+    pub italic: bool,
+    /// Whether the text should be underlined.
+    pub underline: bool,
+    /// The weight of the text.
+    pub weight: FontWeight,
+}
+
+/// A parsed region in a Markdown document.
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedRegion {
+    /// Whether the region is a code block.
+    pub code: bool,
+    /// The link contained in this region, if it has one.
+    pub link: Option<Link>,
+}
+
+/// A Markdown link.
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub enum Link {
+    /// A link to a webpage.
+    Web {
+        /// The URL of the webpage.
+        url: String,
+    },
+    /// A link to a path on the filesystem.
+    Path {
+        /// The path to the item.
+        path: PathBuf,
+    },
+}
+
+impl Link {
+    pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
+        if text.starts_with("http") {
+            return Some(Link::Web { url: text });
+        }
+
+        let path = PathBuf::from(&text);
+        if path.is_absolute() && path.exists() {
+            return Some(Link::Path { path });
+        }
+
+        if let Some(file_location_directory) = file_location_directory {
+            let path = file_location_directory.join(text);
+            if path.exists() {
+                return Some(Link::Path { path });
+            }
+        }
+
+        None
+    }
+}

crates/markdown_preview/src/markdown_parser.rs 🔗

@@ -0,0 +1,1110 @@
+use crate::markdown_elements::*;
+use gpui::FontWeight;
+use pulldown_cmark::{Alignment, Event, Options, Parser, Tag};
+use std::{ops::Range, path::PathBuf};
+
+pub fn parse_markdown(
+    markdown_input: &str,
+    file_location_directory: Option<PathBuf>,
+) -> 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();
+    ParsedMarkdown {
+        children: renderer.parsed,
+    }
+}
+
+struct MarkdownParser<'a> {
+    tokens: Vec<(Event<'a>, Range<usize>)>,
+    /// The current index in the tokens array
+    cursor: usize,
+    /// The blocks that we have successfully parsed so far
+    parsed: Vec<ParsedMarkdownElement>,
+    file_location_directory: Option<PathBuf>,
+}
+
+impl<'a> MarkdownParser<'a> {
+    fn new(
+        tokens: Vec<(Event<'a>, Range<usize>)>,
+        file_location_directory: Option<PathBuf>,
+    ) -> Self {
+        Self {
+            tokens,
+            file_location_directory,
+            cursor: 0,
+            parsed: vec![],
+        }
+    }
+
+    fn eof(&self) -> bool {
+        if self.tokens.is_empty() {
+            return true;
+        }
+        self.cursor >= self.tokens.len() - 1
+    }
+
+    fn peek(&self, steps: usize) -> Option<&(Event, Range<usize>)> {
+        if self.eof() || (steps + self.cursor) >= self.tokens.len() {
+            return self.tokens.last();
+        }
+        return self.tokens.get(self.cursor + steps);
+    }
+
+    fn previous(&self) -> Option<&(Event, Range<usize>)> {
+        if self.cursor == 0 || self.cursor > self.tokens.len() {
+            return None;
+        }
+        return self.tokens.get(self.cursor - 1);
+    }
+
+    fn current(&self) -> Option<&(Event, Range<usize>)> {
+        return self.peek(0);
+    }
+
+    fn is_text_like(event: &Event) -> bool {
+        match event {
+            Event::Text(_)
+            // Represent an inline code block
+            | Event::Code(_)
+            | Event::Html(_)
+            | Event::FootnoteReference(_)
+            | Event::Start(Tag::Link(_, _, _))
+            | Event::Start(Tag::Emphasis)
+            | Event::Start(Tag::Strong)
+            | Event::Start(Tag::Strikethrough)
+            | Event::Start(Tag::Image(_, _, _)) => {
+                return true;
+            }
+            _ => return false,
+        }
+    }
+
+    fn parse_document(mut self) -> Self {
+        while !self.eof() {
+            if let Some(block) = self.parse_block() {
+                self.parsed.push(block);
+            }
+        }
+        self
+    }
+
+    fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
+        let (current, source_range) = self.current().unwrap();
+        match current {
+            Event::Start(tag) => match tag {
+                Tag::Paragraph => {
+                    self.cursor += 1;
+                    let text = self.parse_text(false);
+                    Some(ParsedMarkdownElement::Paragraph(text))
+                }
+                Tag::Heading(level, _, _) => {
+                    let level = level.clone();
+                    self.cursor += 1;
+                    let heading = self.parse_heading(level);
+                    Some(ParsedMarkdownElement::Heading(heading))
+                }
+                Tag::Table(_) => {
+                    self.cursor += 1;
+                    let table = self.parse_table();
+                    Some(ParsedMarkdownElement::Table(table))
+                }
+                Tag::List(order) => {
+                    let order = order.clone();
+                    self.cursor += 1;
+                    let list = self.parse_list(1, order);
+                    Some(ParsedMarkdownElement::List(list))
+                }
+                Tag::BlockQuote => {
+                    self.cursor += 1;
+                    let block_quote = self.parse_block_quote();
+                    Some(ParsedMarkdownElement::BlockQuote(block_quote))
+                }
+                Tag::CodeBlock(kind) => {
+                    let language = match kind {
+                        pulldown_cmark::CodeBlockKind::Indented => None,
+                        pulldown_cmark::CodeBlockKind::Fenced(language) => {
+                            if language.is_empty() {
+                                None
+                            } else {
+                                Some(language.to_string())
+                            }
+                        }
+                    };
+
+                    self.cursor += 1;
+
+                    let code_block = self.parse_code_block(language);
+                    Some(ParsedMarkdownElement::CodeBlock(code_block))
+                }
+                _ => {
+                    self.cursor += 1;
+                    None
+                }
+            },
+            Event::Rule => {
+                let source_range = source_range.clone();
+                self.cursor += 1;
+                Some(ParsedMarkdownElement::HorizontalRule(source_range))
+            }
+            _ => {
+                self.cursor += 1;
+                None
+            }
+        }
+    }
+
+    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();
+
+        let mut text = String::new();
+        let mut bold_depth = 0;
+        let mut italic_depth = 0;
+        let mut link: Option<Link> = None;
+        let mut region_ranges: Vec<Range<usize>> = vec![];
+        let mut regions: Vec<ParsedRegion> = vec![];
+        let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
+
+        loop {
+            if self.eof() {
+                break;
+            }
+
+            let (current, _source_range) = self.current().unwrap();
+            let prev_len = text.len();
+            match current {
+                Event::SoftBreak => {
+                    if should_complete_on_soft_break {
+                        break;
+                    }
+
+                    // `Some text\nSome more text` should be treated as a single line.
+                    text.push(' ');
+                }
+
+                Event::HardBreak => {
+                    break;
+                }
+
+                Event::Text(t) => {
+                    text.push_str(t.as_ref());
+
+                    let mut style = MarkdownHighlightStyle::default();
+
+                    if bold_depth > 0 {
+                        style.weight = FontWeight::BOLD;
+                    }
+
+                    if italic_depth > 0 {
+                        style.italic = true;
+                    }
+
+                    if let Some(link) = link.clone() {
+                        region_ranges.push(prev_len..text.len());
+                        regions.push(ParsedRegion {
+                            code: false,
+                            link: Some(link),
+                        });
+                        style.underline = true;
+                    }
+
+                    if style != MarkdownHighlightStyle::default() {
+                        let mut new_highlight = true;
+                        if let Some((last_range, MarkdownHighlight::Style(last_style))) =
+                            highlights.last_mut()
+                        {
+                            if last_range.end == prev_len && last_style == &style {
+                                last_range.end = text.len();
+                                new_highlight = false;
+                            }
+                        }
+                        if new_highlight {
+                            let range = prev_len..text.len();
+                            highlights.push((range, MarkdownHighlight::Style(style)));
+                        }
+                    }
+                }
+
+                // Note: This event means "inline code" and not "code block"
+                Event::Code(t) => {
+                    text.push_str(t.as_ref());
+                    region_ranges.push(prev_len..text.len());
+
+                    if link.is_some() {
+                        highlights.push((
+                            prev_len..text.len(),
+                            MarkdownHighlight::Style(MarkdownHighlightStyle {
+                                underline: true,
+                                ..Default::default()
+                            }),
+                        ));
+                    }
+
+                    regions.push(ParsedRegion {
+                        code: true,
+                        link: link.clone(),
+                    });
+                }
+
+                Event::Start(tag) => {
+                    match tag {
+                        Tag::Emphasis => italic_depth += 1,
+                        Tag::Strong => bold_depth += 1,
+                        Tag::Link(_type, url, _title) => {
+                            link = Link::identify(
+                                self.file_location_directory.clone(),
+                                url.to_string(),
+                            );
+                        }
+                        Tag::Strikethrough => {
+                            // TODO: Confirm that gpui currently doesn't support strikethroughs
+                        }
+                        _ => {
+                            break;
+                        }
+                    }
+                }
+
+                Event::End(tag) => match tag {
+                    Tag::Emphasis => {
+                        italic_depth -= 1;
+                    }
+                    Tag::Strong => {
+                        bold_depth -= 1;
+                    }
+                    Tag::Link(_, _, _) => {
+                        link = None;
+                    }
+                    Tag::Strikethrough => {
+                        // TODO: Confirm that gpui currently doesn't support strikethroughs
+                    }
+                    Tag::Paragraph => {
+                        self.cursor += 1;
+                        break;
+                    }
+                    _ => {
+                        break;
+                    }
+                },
+
+                _ => {
+                    break;
+                }
+            }
+
+            self.cursor += 1;
+        }
+
+        ParsedMarkdownText {
+            source_range,
+            contents: text,
+            highlights,
+            regions,
+            region_ranges,
+        }
+    }
+
+    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);
+
+        // Advance past the heading end tag
+        self.cursor += 1;
+
+        ParsedMarkdownHeading {
+            source_range: source_range.clone(),
+            level: match level {
+                pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
+                pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
+                pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
+                pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
+                pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
+                pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
+            },
+            contents: text,
+        }
+    }
+
+    fn parse_table(&mut self) -> ParsedMarkdownTable {
+        let (_event, source_range) = self.previous().unwrap();
+        let source_range = source_range.clone();
+        let mut header = ParsedMarkdownTableRow::new();
+        let mut body = vec![];
+        let mut current_row = vec![];
+        let mut in_header = true;
+        let mut alignment: Vec<ParsedMarkdownTableAlignment> = vec![];
+
+        loop {
+            if self.eof() {
+                break;
+            }
+
+            let (current, _source_range) = self.current().unwrap();
+            match current {
+                Event::Start(Tag::TableHead)
+                | Event::Start(Tag::TableRow)
+                | Event::End(Tag::TableCell) => {
+                    self.cursor += 1;
+                }
+                Event::Start(Tag::TableCell) => {
+                    self.cursor += 1;
+                    let cell_contents = self.parse_text(false);
+                    current_row.push(cell_contents);
+                }
+                Event::End(Tag::TableHead) | Event::End(Tag::TableRow) => {
+                    self.cursor += 1;
+                    let new_row = std::mem::replace(&mut current_row, vec![]);
+                    if in_header {
+                        header.children = new_row;
+                        in_header = false;
+                    } else {
+                        let row = ParsedMarkdownTableRow::with_children(new_row);
+                        body.push(row);
+                    }
+                }
+                Event::End(Tag::Table(table_alignment)) => {
+                    alignment = table_alignment
+                        .iter()
+                        .map(|a| Self::convert_alignment(a))
+                        .collect();
+                    self.cursor += 1;
+                    break;
+                }
+                _ => {
+                    break;
+                }
+            }
+        }
+
+        ParsedMarkdownTable {
+            source_range,
+            header,
+            body,
+            column_alignments: alignment,
+        }
+    }
+
+    fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment {
+        match alignment {
+            Alignment::None => ParsedMarkdownTableAlignment::None,
+            Alignment::Left => ParsedMarkdownTableAlignment::Left,
+            Alignment::Center => ParsedMarkdownTableAlignment::Center,
+            Alignment::Right => ParsedMarkdownTableAlignment::Right,
+        }
+    }
+
+    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;
+
+        let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![];
+
+        while !self.eof() {
+            let (current, _source_range) = self.current().unwrap();
+            match current {
+                Event::Start(Tag::List(order)) => {
+                    let order = order.clone();
+                    self.cursor += 1;
+
+                    let inner_list = self.parse_list(depth + 1, order);
+                    let block = ParsedMarkdownElement::List(inner_list);
+                    current_list_items.push(Box::new(block));
+                }
+                Event::End(Tag::List(_)) => {
+                    self.cursor += 1;
+                    break;
+                }
+                Event::Start(Tag::Item) => {
+                    self.cursor += 1;
+                    inside_list_item = true;
+
+                    // Check for task list marker (`- [ ]` or `- [x]`)
+                    if let Some(next) = self.current() {
+                        match next.0 {
+                            Event::TaskListMarker(checked) => {
+                                task_item = Some(checked);
+                                self.cursor += 1;
+                            }
+                            _ => {}
+                        }
+                    }
+
+                    if let Some(next) = self.current() {
+                        // This is a plain list item.
+                        // For example `- some text` or `1. [Docs](./docs.md)`
+                        if MarkdownParser::is_text_like(&next.0) {
+                            let text = self.parse_text(false);
+                            let block = ParsedMarkdownElement::Paragraph(text);
+                            current_list_items.push(Box::new(block));
+                        } else {
+                            let block = self.parse_block();
+                            if let Some(block) = block {
+                                current_list_items.push(Box::new(block));
+                            }
+                        }
+                    }
+                }
+                Event::End(Tag::Item) => {
+                    self.cursor += 1;
+
+                    let item_type = if let Some(checked) = task_item {
+                        ParsedMarkdownListItemType::Task(checked)
+                    } else if let Some(order) = order.clone() {
+                        ParsedMarkdownListItemType::Ordered(order)
+                    } else {
+                        ParsedMarkdownListItemType::Unordered
+                    };
+
+                    if let Some(current) = order {
+                        order = Some(current + 1);
+                    }
+
+                    let contents = std::mem::replace(&mut current_list_items, vec![]);
+
+                    children.push(ParsedMarkdownListItem {
+                        contents,
+                        depth,
+                        item_type,
+                    });
+
+                    inside_list_item = false;
+                    task_item = None;
+                }
+                _ => {
+                    if !inside_list_item {
+                        break;
+                    }
+
+                    let block = self.parse_block();
+                    if let Some(block) = block {
+                        current_list_items.push(Box::new(block));
+                    }
+                }
+            }
+        }
+
+        ParsedMarkdownList {
+            source_range,
+            children,
+        }
+    }
+
+    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;
+
+        let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
+
+        while !self.eof() {
+            let block = self.parse_block();
+
+            if let Some(block) = block {
+                children.push(Box::new(block));
+            } else {
+                break;
+            }
+
+            if self.eof() {
+                break;
+            }
+
+            let (current, _source_range) = self.current().unwrap();
+            match current {
+                // This is a nested block quote.
+                // Record that we're in a nested block quote and continue parsing.
+                // We don't need to advance the cursor since the next
+                // call to `parse_block` will handle it.
+                Event::Start(Tag::BlockQuote) => {
+                    nested_depth += 1;
+                }
+                Event::End(Tag::BlockQuote) => {
+                    nested_depth -= 1;
+                    if nested_depth == 0 {
+                        self.cursor += 1;
+                        break;
+                    }
+                }
+                _ => {}
+            };
+        }
+
+        ParsedMarkdownBlockQuote {
+            source_range,
+            children,
+        }
+    }
+
+    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();
+
+        while !self.eof() {
+            let (current, _source_range) = self.current().unwrap();
+            match current {
+                Event::Text(text) => {
+                    code.push_str(&text);
+                    self.cursor += 1;
+                }
+                Event::End(Tag::CodeBlock(_)) => {
+                    self.cursor += 1;
+                    break;
+                }
+                _ => {
+                    break;
+                }
+            }
+        }
+
+        ParsedMarkdownCodeBlock {
+            source_range,
+            contents: code.trim().to_string().into(),
+            language,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use pretty_assertions::assert_eq;
+
+    use ParsedMarkdownElement::*;
+    use ParsedMarkdownListItemType::*;
+
+    fn parse(input: &str) -> ParsedMarkdown {
+        parse_markdown(input, None)
+    }
+
+    #[test]
+    fn test_headings() {
+        let parsed = parse("# Heading one\n## Heading two\n### Heading three");
+
+        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),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_newlines_dont_new_paragraphs() {
+        let parsed = parse("Some text **that is bolded**\n and *italicized*");
+
+        assert_eq!(
+            parsed.children,
+            vec![p("Some text that is bolded and italicized", 0..46)]
+        );
+    }
+
+    #[test]
+    fn test_heading_with_paragraph() {
+        let parsed = parse("# Zed\nThe editor");
+
+        assert_eq!(
+            parsed.children,
+            vec![h1(text("Zed", 0..6), 0..6), p("The editor", 6..16),]
+        );
+    }
+
+    #[test]
+    fn test_double_newlines_do_new_paragraphs() {
+        let parsed = parse("Some text **that is bolded**\n\n and *italicized*");
+
+        assert_eq!(
+            parsed.children,
+            vec![
+                p("Some text that is bolded", 0..29),
+                p("and italicized", 31..47),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_bold_italic_text() {
+        let parsed = parse("Some text **that is bolded** and *italicized*");
+
+        assert_eq!(
+            parsed.children,
+            vec![p("Some text that is bolded and italicized", 0..45)]
+        );
+    }
+
+    #[test]
+    fn test_header_only_table() {
+        let markdown = "\
+| Header 1 | Header 2 |
+|----------|----------|
+
+Some other content
+";
+
+        let expected_table = table(
+            0..48,
+            row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
+            vec![],
+        );
+
+        assert_eq!(
+            parse(markdown).children[0],
+            ParsedMarkdownElement::Table(expected_table)
+        );
+    }
+
+    #[test]
+    fn test_basic_table() {
+        let markdown = "\
+| Header 1 | Header 2 |
+|----------|----------|
+| Cell 1   | Cell 2   |
+| Cell 3   | Cell 4   |";
+
+        let expected_table = table(
+            0..95,
+            row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
+            vec![
+                row(vec![text("Cell 1", 49..59), text("Cell 2", 60..70)]),
+                row(vec![text("Cell 3", 73..83), text("Cell 4", 84..94)]),
+            ],
+        );
+
+        assert_eq!(
+            parse(markdown).children[0],
+            ParsedMarkdownElement::Table(expected_table)
+        );
+    }
+
+    #[test]
+    fn test_list_basic() {
+        let parsed = parse(
+            "\
+* Item 1
+* Item 2
+* Item 3
+",
+        );
+
+        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
+            ),]
+        );
+    }
+
+    #[test]
+    fn test_list_with_tasks() {
+        let parsed = parse(
+            "\
+- [ ] TODO
+- [x] Checked
+",
+        );
+
+        assert_eq!(
+            parsed.children,
+            vec![list(
+                vec![
+                    list_item(1, Task(false), vec![p("TODO", 2..5)]),
+                    list_item(1, Task(true), vec![p("Checked", 13..16)]),
+                ],
+                0..25
+            ),]
+        );
+    }
+
+    #[test]
+    fn test_list_nested() {
+        let parsed = parse(
+            "\
+* Item 1
+* Item 2
+* Item 3
+
+1. Hello
+1. Two
+   1. Three
+2. Four
+3. Five
+
+* First
+  1. Hello
+     1. Goodbyte
+        - Inner
+        - Inner
+  2. Goodbyte
+* Last
+",
+        );
+
+        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
+                ),
+            ]
+        );
+    }
+
+    #[test]
+    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.",
+        );
+
+        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)
+                    ],
+                ),],
+                0..96,
+            ),]
+        );
+    }
+
+    #[test]
+    fn test_list_with_leading_text() {
+        let parsed = parse(
+            "\
+* `code`
+* **bold**
+* [link](https://example.com)
+",
+        );
+
+        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,
+            ),]
+        );
+    }
+
+    #[test]
+    fn test_simple_block_quote() {
+        let parsed = parse("> Simple block quote with **styled text**");
+
+        assert_eq!(
+            parsed.children,
+            vec![block_quote(
+                vec![p("Simple block quote with styled text", 2..41)],
+                0..41
+            )]
+        );
+    }
+
+    #[test]
+    fn test_simple_block_quote_with_multiple_lines() {
+        let parsed = parse(
+            "\
+> # Heading
+> More
+> text
+>
+> More text
+",
+        );
+
+        assert_eq!(
+            parsed.children,
+            vec![block_quote(
+                vec![
+                    h1(text("Heading", 2..12), 2..12),
+                    p("More text", 14..26),
+                    p("More text", 30..40)
+                ],
+                0..40
+            )]
+        );
+    }
+
+    #[test]
+    fn test_nested_block_quote() {
+        let parsed = parse(
+            "\
+> A
+>
+> > # B
+>
+> C
+
+More text
+",
+        );
+
+        assert_eq!(
+            parsed.children,
+            vec![
+                block_quote(
+                    vec![
+                        p("A", 2..4),
+                        block_quote(vec![h1(text("B", 10..14), 10..14)], 8..14),
+                        p("C", 18..20)
+                    ],
+                    0..20
+                ),
+                p("More text", 21..31)
+            ]
+        );
+    }
+
+    #[test]
+    fn test_code_block() {
+        let parsed = parse(
+            "\
+```
+fn main() {
+    return 0;
+}
+```
+",
+        );
+
+        assert_eq!(
+            parsed.children,
+            vec![code_block(None, "fn main() {\n    return 0;\n}", 0..35)]
+        );
+    }
+
+    #[test]
+    fn test_code_block_with_language() {
+        let parsed = parse(
+            "\
+```rust
+fn main() {
+    return 0;
+}
+```
+",
+        );
+
+        assert_eq!(
+            parsed.children,
+            vec![code_block(
+                Some("rust".into()),
+                "fn main() {\n    return 0;\n}",
+                0..39
+            )]
+        );
+    }
+
+    fn h1(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
+        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
+            source_range,
+            level: HeadingLevel::H1,
+            contents,
+        })
+    }
+
+    fn h2(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
+        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
+            source_range,
+            level: HeadingLevel::H2,
+            contents,
+        })
+    }
+
+    fn h3(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
+        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
+            source_range,
+            level: HeadingLevel::H3,
+            contents,
+        })
+    }
+
+    fn p(contents: &str, source_range: Range<usize>) -> ParsedMarkdownElement {
+        ParsedMarkdownElement::Paragraph(text(contents, source_range))
+    }
+
+    fn text(contents: &str, source_range: Range<usize>) -> ParsedMarkdownText {
+        ParsedMarkdownText {
+            highlights: Vec::new(),
+            region_ranges: Vec::new(),
+            regions: Vec::new(),
+            source_range,
+            contents: contents.to_string(),
+        }
+    }
+
+    fn block_quote(
+        children: Vec<ParsedMarkdownElement>,
+        source_range: Range<usize>,
+    ) -> ParsedMarkdownElement {
+        ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
+            source_range,
+            children: children.into_iter().map(Box::new).collect(),
+        })
+    }
+
+    fn code_block(
+        language: Option<String>,
+        code: &str,
+        source_range: Range<usize>,
+    ) -> ParsedMarkdownElement {
+        ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
+            source_range,
+            language,
+            contents: code.to_string().into(),
+        })
+    }
+
+    fn list(
+        children: Vec<ParsedMarkdownListItem>,
+        source_range: Range<usize>,
+    ) -> ParsedMarkdownElement {
+        List(ParsedMarkdownList {
+            source_range,
+            children,
+        })
+    }
+
+    fn list_item(
+        depth: u16,
+        item_type: ParsedMarkdownListItemType,
+        contents: Vec<ParsedMarkdownElement>,
+    ) -> ParsedMarkdownListItem {
+        ParsedMarkdownListItem {
+            item_type,
+            depth,
+            contents: contents.into_iter().map(Box::new).collect(),
+        }
+    }
+
+    fn table(
+        source_range: Range<usize>,
+        header: ParsedMarkdownTableRow,
+        body: Vec<ParsedMarkdownTableRow>,
+    ) -> ParsedMarkdownTable {
+        ParsedMarkdownTable {
+            column_alignments: Vec::new(),
+            source_range,
+            header,
+            body,
+        }
+    }
+
+    fn row(children: Vec<ParsedMarkdownText>) -> ParsedMarkdownTableRow {
+        ParsedMarkdownTableRow { children }
+    }
+
+    impl PartialEq for ParsedMarkdownTable {
+        fn eq(&self, other: &Self) -> bool {
+            self.source_range == other.source_range
+                && self.header == other.header
+                && self.body == other.body
+        }
+    }
+
+    impl PartialEq for ParsedMarkdownText {
+        fn eq(&self, other: &Self) -> bool {
+            self.source_range == other.source_range && self.contents == other.contents
+        }
+    }
+}

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -1,35 +1,41 @@
+use std::{ops::Range, path::PathBuf};
+
 use editor::{Editor, EditorEvent};
 use gpui::{
-    canvas, AnyElement, AppContext, AvailableSpace, EventEmitter, FocusHandle, FocusableView,
-    InteractiveElement, IntoElement, ParentElement, Render, Styled, View, ViewContext,
+    list, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
 };
-use language::LanguageRegistry;
-use std::sync::Arc;
 use ui::prelude::*;
 use workspace::item::Item;
 use workspace::Workspace;
 
-use crate::{markdown_renderer::render_markdown, OpenPreview};
+use crate::{
+    markdown_elements::ParsedMarkdown,
+    markdown_parser::parse_markdown,
+    markdown_renderer::{render_markdown_block, RenderContext},
+    OpenPreview,
+};
 
 pub struct MarkdownPreviewView {
+    workspace: WeakView<Workspace>,
     focus_handle: FocusHandle,
-    languages: Arc<LanguageRegistry>,
-    contents: String,
+    contents: ParsedMarkdown,
+    selected_block: usize,
+    list_state: ListState,
 }
 
 impl MarkdownPreviewView {
     pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
-        let languages = workspace.app_state().languages.clone();
-
         workspace.register_action(move |workspace, _: &OpenPreview, cx| {
             if workspace.has_active_modal(cx) {
                 cx.propagate();
                 return;
             }
-            let languages = languages.clone();
+
             if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
+                let workspace_handle = workspace.weak_handle();
                 let view: View<MarkdownPreviewView> =
-                    cx.new_view(|cx| MarkdownPreviewView::new(editor, languages, cx));
+                    MarkdownPreviewView::new(editor, workspace_handle, cx);
                 workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
                 cx.notify();
             }
@@ -38,29 +44,120 @@ impl MarkdownPreviewView {
 
     pub fn new(
         active_editor: View<Editor>,
-        languages: Arc<LanguageRegistry>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let focus_handle = cx.focus_handle();
-
-        cx.subscribe(&active_editor, |this, editor, event: &EditorEvent, cx| {
-            if *event == EditorEvent::Edited {
-                let editor = editor.read(cx);
-                let contents = editor.buffer().read(cx).snapshot(cx).text();
-                this.contents = contents;
-                cx.notify();
+        workspace: WeakView<Workspace>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> View<Self> {
+        cx.new_view(|cx: &mut ViewContext<Self>| {
+            let view = cx.view().downgrade();
+            let editor = active_editor.read(cx);
+
+            let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx);
+            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| {
+                match event {
+                    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);
+                        this.contents = parse_markdown(&contents, file_location);
+                        this.list_state.reset(this.contents.children.len());
+                        cx.notify();
+
+                        // TODO: This does not work as expected.
+                        // The scroll request appears to be dropped
+                        // after `.reset` is called.
+                        this.list_state.scroll_to_reveal_item(this.selected_block);
+                        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();
+
+            let list_state = ListState::new(
+                contents.children.len(),
+                gpui::ListAlignment::Top,
+                px(1000.),
+                move |ix, cx| {
+                    if let Some(view) = view.upgrade() {
+                        view.update(cx, |view, cx| {
+                            let mut render_cx =
+                                RenderContext::new(Some(view.workspace.clone()), cx);
+                            let block = view.contents.children.get(ix).unwrap();
+                            let block = render_markdown_block(block, &mut render_cx);
+                            let block = div().child(block).pl_4().pb_3();
+
+                            if ix == view.selected_block {
+                                let indicator = div()
+                                    .h_full()
+                                    .w(px(4.0))
+                                    .bg(cx.theme().colors().border)
+                                    .rounded_sm();
+
+                                return div()
+                                    .relative()
+                                    .child(block)
+                                    .child(indicator.absolute().left_0().top_0())
+                                    .into_any();
+                            }
+
+                            block.into_any()
+                        })
+                    } else {
+                        div().into_any()
+                    }
+                },
+            );
+
+            Self {
+                selected_block: 0,
+                focus_handle: cx.focus_handle(),
+                workspace,
+                contents,
+                list_state,
             }
         })
-        .detach();
+    }
 
-        let editor = active_editor.read(cx);
-        let contents = editor.buffer().read(cx).snapshot(cx).text();
+    /// The absolute path of the file that is currently being previewed.
+    fn get_folder_for_active_editor(
+        editor: &Editor,
+        cx: &ViewContext<MarkdownPreviewView>,
+    ) -> Option<PathBuf> {
+        if let Some(file) = editor.file_at(0, cx) {
+            if let Some(file) = file.as_local() {
+                file.abs_path(cx).parent().map(|p| p.to_path_buf())
+            } else {
+                None
+            }
+        } else {
+            None
+        }
+    }
 
-        Self {
-            focus_handle,
-            languages,
-            contents,
+    fn get_block_index_under_cursor(&self, selection_range: Range<usize>) -> usize {
+        let mut block_index = 0;
+        let cursor = selection_range.start;
+
+        for (i, block) in self.contents.children.iter().enumerate() {
+            let Range { start, end } = block.source_range();
+            if start <= cursor && end >= cursor {
+                block_index = i;
+                break;
+            }
         }
+
+        return block_index;
     }
 }
 
@@ -108,30 +205,17 @@ impl Item for MarkdownPreviewView {
 
 impl Render for MarkdownPreviewView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let rendered_markdown = v_flex()
-            .items_start()
-            .justify_start()
+        v_flex()
+            .id("MarkdownPreview")
             .key_context("MarkdownPreview")
             .track_focus(&self.focus_handle)
-            .id("MarkdownPreview")
-            .overflow_y_scroll()
-            .overflow_x_hidden()
-            .size_full()
+            .full()
             .bg(cx.theme().colors().editor_background)
             .p_4()
-            .children(render_markdown(&self.contents, &self.languages, cx));
-
-        div().flex_1().child(
-            // FIXME: This shouldn't be necessary
-            // but the overflow_scroll above doesn't seem to work without it
-            canvas(move |bounds, cx| {
-                rendered_markdown.into_any().draw(
-                    bounds.origin,
-                    bounds.size.map(AvailableSpace::Definite),
-                    cx,
-                )
-            })
-            .size_full(),
-        )
+            .child(
+                div()
+                    .flex_grow()
+                    .map(|this| this.child(list(self.list_state.clone()).full())),
+            )
     }
 }

crates/markdown_preview/src/markdown_renderer.rs 🔗

@@ -1,346 +1,322 @@
-use std::{ops::Range, sync::Arc};
-
+use crate::markdown_elements::{
+    HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
+    ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownList, ParsedMarkdownListItemType,
+    ParsedMarkdownTable, ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
+};
 use gpui::{
-    div, px, rems, AnyElement, DefiniteLength, Div, ElementId, Hsla, ParentElement, SharedString,
-    Styled, StyledText, WindowContext,
+    div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
+    HighlightStyle, Hsla, InteractiveText, IntoElement, ParentElement, SharedString, Styled,
+    StyledText, TextStyle, WeakView, WindowContext,
 };
-use language::LanguageRegistry;
-use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
-use rich_text::render_rich_text;
-use theme::{ActiveTheme, Theme};
-use ui::{h_flex, v_flex};
-
-enum TableState {
-    Header,
-    Body,
-}
-
-struct MarkdownTable {
-    column_alignments: Vec<Alignment>,
-    header: Vec<Div>,
-    body: Vec<Vec<Div>>,
-    current_row: Vec<Div>,
-    state: TableState,
+use std::{ops::Range, sync::Arc};
+use theme::{ActiveTheme, SyntaxTheme};
+use ui::{h_flex, v_flex, Label};
+use workspace::Workspace;
+
+pub struct RenderContext {
+    workspace: Option<WeakView<Workspace>>,
+    next_id: usize,
+    text_style: TextStyle,
     border_color: Hsla,
+    text_color: Hsla,
+    text_muted_color: Hsla,
+    code_block_background_color: Hsla,
+    code_span_background_color: Hsla,
+    syntax_theme: Arc<SyntaxTheme>,
+    indent: usize,
 }
 
-impl MarkdownTable {
-    fn new(border_color: Hsla, column_alignments: Vec<Alignment>) -> Self {
-        Self {
-            column_alignments,
-            header: Vec::new(),
-            body: Vec::new(),
-            current_row: Vec::new(),
-            state: TableState::Header,
-            border_color,
+impl RenderContext {
+    pub fn new(workspace: Option<WeakView<Workspace>>, cx: &WindowContext) -> RenderContext {
+        let theme = cx.theme().clone();
+
+        RenderContext {
+            workspace,
+            next_id: 0,
+            indent: 0,
+            text_style: cx.text_style(),
+            syntax_theme: theme.syntax().clone(),
+            border_color: theme.colors().border,
+            text_color: theme.colors().text,
+            text_muted_color: theme.colors().text_muted,
+            code_block_background_color: theme.colors().surface_background,
+            code_span_background_color: theme.colors().editor_document_highlight_read_background,
         }
     }
 
-    fn finish_row(&mut self) {
-        match self.state {
-            TableState::Header => {
-                self.header.extend(self.current_row.drain(..));
-                self.state = TableState::Body;
-            }
-            TableState::Body => {
-                self.body.push(self.current_row.drain(..).collect());
-            }
-        }
+    fn next_id(&mut self, span: &Range<usize>) -> ElementId {
+        let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end);
+        self.next_id += 1;
+        ElementId::from(SharedString::from(id))
     }
 
-    fn add_cell(&mut self, contents: AnyElement) {
-        let container = match self.alignment_for_next_cell() {
-            Alignment::Left | Alignment::None => div(),
-            Alignment::Center => v_flex().items_center(),
-            Alignment::Right => v_flex().items_end(),
-        };
-
-        let cell = container
-            .w_full()
-            .child(contents)
-            .px_2()
-            .py_1()
-            .border_color(self.border_color);
-
-        let cell = match self.state {
-            TableState::Header => cell.border_2(),
-            TableState::Body => cell.border_1(),
-        };
-
-        self.current_row.push(cell);
+    /// This ensures that children inside of block quotes
+    /// have padding between them.
+    ///
+    /// For example, for this markdown:
+    ///
+    /// ```markdown
+    /// > This is a block quote.
+    /// >
+    /// > And this is the next paragraph.
+    /// ```
+    ///
+    /// We give padding between "This is a block quote."
+    /// and "And this is the next paragraph."
+    fn with_common_p(&self, element: Div) -> Div {
+        if self.indent > 0 {
+            element.pb_3()
+        } else {
+            element
+        }
     }
+}
 
-    fn finish(self) -> Div {
-        let mut table = v_flex().w_full();
-        let mut header = h_flex();
+pub fn render_parsed_markdown(
+    parsed: &ParsedMarkdown,
+    workspace: Option<WeakView<Workspace>>,
+    cx: &WindowContext,
+) -> Vec<AnyElement> {
+    let mut cx = RenderContext::new(workspace, cx);
+    let mut elements = Vec::new();
 
-        for cell in self.header {
-            header = header.child(cell);
-        }
-        table = table.child(header);
-        for row in self.body {
-            let mut row_div = h_flex();
-            for cell in row {
-                row_div = row_div.child(cell);
-            }
-            table = table.child(row_div);
-        }
-        table
+    for child in &parsed.children {
+        elements.push(render_markdown_block(child, &mut cx));
     }
 
-    fn alignment_for_next_cell(&self) -> Alignment {
-        self.column_alignments
-            .get(self.current_row.len())
-            .copied()
-            .unwrap_or(Alignment::None)
+    return elements;
+}
+
+pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement {
+    use ParsedMarkdownElement::*;
+    match block {
+        Paragraph(text) => render_markdown_paragraph(text, cx),
+        Heading(heading) => render_markdown_heading(heading, cx),
+        List(list) => render_markdown_list(list, 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),
+        HorizontalRule(_) => render_markdown_rule(cx),
     }
 }
 
-struct Renderer<I> {
-    source_contents: String,
-    iter: I,
-    theme: Arc<Theme>,
-    finished: Vec<Div>,
-    language_registry: Arc<LanguageRegistry>,
-    table: Option<MarkdownTable>,
-    list_depth: usize,
-    block_quote_depth: usize,
+fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContext) -> AnyElement {
+    let size = match parsed.level {
+        HeadingLevel::H1 => rems(2.),
+        HeadingLevel::H2 => rems(1.5),
+        HeadingLevel::H3 => rems(1.25),
+        HeadingLevel::H4 => rems(1.),
+        HeadingLevel::H5 => rems(0.875),
+        HeadingLevel::H6 => rems(0.85),
+    };
+
+    let color = match parsed.level {
+        HeadingLevel::H6 => cx.text_muted_color,
+        _ => cx.text_color,
+    };
+
+    let line_height = DefiniteLength::from(rems(1.25));
+
+    div()
+        .line_height(line_height)
+        .text_size(size)
+        .text_color(color)
+        .pt(rems(0.15))
+        .pb_1()
+        .child(render_markdown_text(&parsed.contents, cx))
+        .into_any()
 }
 
-impl<'a, I> Renderer<I>
-where
-    I: Iterator<Item = (Event<'a>, Range<usize>)>,
-{
-    fn new(
-        iter: I,
-        source_contents: String,
-        language_registry: &Arc<LanguageRegistry>,
-        theme: Arc<Theme>,
-    ) -> Self {
-        Self {
-            iter,
-            source_contents,
-            theme,
-            table: None,
-            finished: vec![],
-            language_registry: language_registry.clone(),
-            list_depth: 0,
-            block_quote_depth: 0,
-        }
-    }
+fn render_markdown_list(parsed: &ParsedMarkdownList, cx: &mut RenderContext) -> AnyElement {
+    use ParsedMarkdownListItemType::*;
 
-    fn run(mut self, cx: &WindowContext) -> Self {
-        while let Some((event, source_range)) = self.iter.next() {
-            match event {
-                Event::Start(tag) => {
-                    self.start_tag(tag);
-                }
-                Event::End(tag) => {
-                    self.end_tag(tag, source_range, cx);
-                }
-                Event::Rule => {
-                    let rule = div().w_full().h(px(2.)).bg(self.theme.colors().border);
-                    self.finished.push(div().mb_4().child(rule));
-                }
-                _ => {}
-            }
-        }
-        self
-    }
+    let mut items = vec![];
+    for item in &parsed.children {
+        let padding = rems((item.depth - 1) as f32 * 0.25);
 
-    fn start_tag(&mut self, tag: Tag<'a>) {
-        match tag {
-            Tag::List(_) => {
-                self.list_depth += 1;
-            }
-            Tag::BlockQuote => {
-                self.block_quote_depth += 1;
-            }
-            Tag::Table(column_alignments) => {
-                self.table = Some(MarkdownTable::new(
-                    self.theme.colors().border,
-                    column_alignments,
-                ));
-            }
-            _ => {}
-        }
-    }
+        let bullet = match item.item_type {
+            Ordered(order) => format!("{}.", order),
+            Unordered => "•".to_string(),
+            Task(checked) => if checked { "☑" } else { "☐" }.to_string(),
+        };
+        let bullet = div().mr_2().child(Label::new(bullet));
 
-    fn end_tag(&mut self, tag: Tag, source_range: Range<usize>, cx: &WindowContext) {
-        match tag {
-            Tag::Paragraph => {
-                if self.list_depth > 0 || self.block_quote_depth > 0 {
-                    return;
-                }
+        let contents: Vec<AnyElement> = item
+            .contents
+            .iter()
+            .map(|c| render_markdown_block(c.as_ref(), cx))
+            .collect();
 
-                let element = self.render_md_from_range(source_range.clone(), cx);
-                let paragraph = div().mb_3().child(element);
+        let item = h_flex()
+            .pl(DefiniteLength::Absolute(AbsoluteLength::Rems(padding)))
+            .items_start()
+            .children(vec![bullet, div().children(contents).pr_2().w_full()]);
 
-                self.finished.push(paragraph);
-            }
-            Tag::Heading(level, _, _) => {
-                let mut headline = self.headline(level);
-                if source_range.start > 0 {
-                    headline = headline.mt_4();
-                }
+        items.push(item);
+    }
 
-                let element = self.render_md_from_range(source_range.clone(), cx);
-                let headline = headline.child(element);
+    cx.with_common_p(div()).children(items).into_any()
+}
 
-                self.finished.push(headline);
-            }
-            Tag::List(_) => {
-                if self.list_depth == 1 {
-                    let element = self.render_md_from_range(source_range.clone(), cx);
-                    let list = div().mb_3().child(element);
+fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
+    let header = render_markdown_table_row(&parsed.header, &parsed.column_alignments, true, cx);
 
-                    self.finished.push(list);
-                }
+    let body: Vec<AnyElement> = parsed
+        .body
+        .iter()
+        .map(|row| render_markdown_table_row(row, &parsed.column_alignments, false, cx))
+        .collect();
 
-                self.list_depth -= 1;
-            }
-            Tag::BlockQuote => {
-                let element = self.render_md_from_range(source_range.clone(), cx);
-
-                let block_quote = h_flex()
-                    .mb_3()
-                    .child(
-                        div()
-                            .w(px(4.))
-                            .bg(self.theme.colors().border)
-                            .h_full()
-                            .mr_2()
-                            .mt_1(),
-                    )
-                    .text_color(self.theme.colors().text_muted)
-                    .child(element);
-
-                self.finished.push(block_quote);
-
-                self.block_quote_depth -= 1;
-            }
-            Tag::CodeBlock(kind) => {
-                let contents = self.source_contents[source_range.clone()].trim();
-                let contents = contents.trim_start_matches("```");
-                let contents = contents.trim_end_matches("```");
-                let contents = match kind {
-                    CodeBlockKind::Fenced(language) => {
-                        contents.trim_start_matches(&language.to_string())
-                    }
-                    CodeBlockKind::Indented => contents,
-                };
-                let contents: String = contents.into();
-                let contents = SharedString::from(contents);
-
-                let code_block = div()
-                    .mb_3()
-                    .px_4()
-                    .py_0()
-                    .bg(self.theme.colors().surface_background)
-                    .child(StyledText::new(contents));
-
-                self.finished.push(code_block);
-            }
-            Tag::Table(_alignment) => {
-                if self.table.is_none() {
-                    log::error!("Table end without table ({:?})", source_range);
-                    return;
-                }
+    cx.with_common_p(v_flex())
+        .w_full()
+        .child(header)
+        .children(body)
+        .into_any()
+}
 
-                let table = self.table.take().unwrap();
-                let table = table.finish().mb_4();
-                self.finished.push(table);
-            }
-            Tag::TableHead => {
-                if self.table.is_none() {
-                    log::error!("Table head without table ({:?})", source_range);
-                    return;
-                }
+fn render_markdown_table_row(
+    parsed: &ParsedMarkdownTableRow,
+    alignments: &Vec<ParsedMarkdownTableAlignment>,
+    is_header: bool,
+    cx: &mut RenderContext,
+) -> AnyElement {
+    let mut items = vec![];
+
+    for cell in &parsed.children {
+        let alignment = alignments
+            .get(items.len())
+            .copied()
+            .unwrap_or(ParsedMarkdownTableAlignment::None);
 
-                self.table.as_mut().unwrap().finish_row();
-            }
-            Tag::TableRow => {
-                if self.table.is_none() {
-                    log::error!("Table row without table ({:?})", source_range);
-                    return;
-                }
+        let contents = render_markdown_text(cell, cx);
 
-                self.table.as_mut().unwrap().finish_row();
-            }
-            Tag::TableCell => {
-                if self.table.is_none() {
-                    log::error!("Table cell without table ({:?})", source_range);
-                    return;
-                }
+        let container = match alignment {
+            ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
+            ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
+            ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
+        };
 
-                let contents = self.render_md_from_range(source_range.clone(), cx);
-                self.table.as_mut().unwrap().add_cell(contents);
-            }
-            _ => {}
+        let mut cell = container
+            .w_full()
+            .child(contents)
+            .px_2()
+            .py_1()
+            .border_color(cx.border_color);
+
+        if is_header {
+            cell = cell.border_2()
+        } else {
+            cell = cell.border_1()
         }
-    }
 
-    fn render_md_from_range(
-        &self,
-        source_range: Range<usize>,
-        cx: &WindowContext,
-    ) -> gpui::AnyElement {
-        let mentions = &[];
-        let language = None;
-        let paragraph = &self.source_contents[source_range.clone()];
-        let rich_text = render_rich_text(
-            paragraph.into(),
-            mentions,
-            &self.language_registry,
-            language,
-        );
-        let id: ElementId = source_range.start.into();
-        rich_text.element(id, cx)
+        items.push(cell);
     }
 
-    fn headline(&self, level: HeadingLevel) -> Div {
-        let size = match level {
-            HeadingLevel::H1 => rems(2.),
-            HeadingLevel::H2 => rems(1.5),
-            HeadingLevel::H3 => rems(1.25),
-            HeadingLevel::H4 => rems(1.),
-            HeadingLevel::H5 => rems(0.875),
-            HeadingLevel::H6 => rems(0.85),
-        };
+    h_flex().children(items).into_any_element()
+}
 
-        let color = match level {
-            HeadingLevel::H6 => self.theme.colors().text_muted,
-            _ => self.theme.colors().text,
-        };
+fn render_markdown_block_quote(
+    parsed: &ParsedMarkdownBlockQuote,
+    cx: &mut RenderContext,
+) -> AnyElement {
+    cx.indent += 1;
+
+    let children: Vec<AnyElement> = parsed
+        .children
+        .iter()
+        .map(|child| render_markdown_block(child, cx))
+        .collect();
+
+    cx.indent -= 1;
+
+    cx.with_common_p(div())
+        .child(
+            div()
+                .border_l_4()
+                .border_color(cx.border_color)
+                .pl_3()
+                .children(children),
+        )
+        .into_any()
+}
 
-        let line_height = DefiniteLength::from(rems(1.25));
+fn render_markdown_code_block(
+    parsed: &ParsedMarkdownCodeBlock,
+    cx: &mut RenderContext,
+) -> AnyElement {
+    cx.with_common_p(div())
+        .px_3()
+        .py_3()
+        .bg(cx.code_block_background_color)
+        .child(StyledText::new(parsed.contents.clone()))
+        .into_any()
+}
 
-        let headline = h_flex()
-            .w_full()
-            .line_height(line_height)
-            .text_size(size)
-            .text_color(color)
-            .mb_4()
-            .pb(rems(0.15));
+fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
+    cx.with_common_p(div())
+        .child(render_markdown_text(parsed, cx))
+        .into_any_element()
+}
 
-        headline
+fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
+    let element_id = cx.next_id(&parsed.source_range);
+
+    let highlights = gpui::combine_highlights(
+        parsed.highlights.iter().filter_map(|(range, highlight)| {
+            let highlight = highlight.to_highlight_style(&cx.syntax_theme)?;
+            Some((range.clone(), highlight))
+        }),
+        parsed
+            .regions
+            .iter()
+            .zip(&parsed.region_ranges)
+            .filter_map(|(region, range)| {
+                if region.code {
+                    Some((
+                        range.clone(),
+                        HighlightStyle {
+                            background_color: Some(cx.code_span_background_color),
+                            ..Default::default()
+                        },
+                    ))
+                } else {
+                    None
+                }
+            }),
+    );
+
+    let mut links = Vec::new();
+    let mut link_ranges = Vec::new();
+    for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
+        if let Some(link) = region.link.clone() {
+            links.push(link);
+            link_ranges.push(range.clone());
+        }
     }
+
+    let workspace = cx.workspace.clone();
+
+    InteractiveText::new(
+        element_id,
+        StyledText::new(parsed.contents.clone()).with_highlights(&cx.text_style, highlights),
+    )
+    .on_click(
+        link_ranges,
+        move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
+            Link::Web { url } => window_cx.open_url(url),
+            Link::Path { path } => {
+                if let Some(workspace) = &workspace {
+                    _ = workspace.update(window_cx, |workspace, cx| {
+                        workspace.open_abs_path(path.clone(), false, cx).detach();
+                    });
+                }
+            }
+        },
+    )
+    .into_any_element()
 }
 
-pub fn render_markdown(
-    markdown_input: &str,
-    language_registry: &Arc<LanguageRegistry>,
-    cx: &WindowContext,
-) -> Vec<Div> {
-    let theme = cx.theme().clone();
-    let options = Options::all();
-    let parser = Parser::new_ext(markdown_input, options);
-    let renderer = Renderer::new(
-        parser.into_offset_iter(),
-        markdown_input.to_owned(),
-        language_registry,
-        theme,
-    );
-    let renderer = renderer.run(cx);
-    return renderer.finished;
+fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
+    let rule = div().w_full().h(px(2.)).bg(cx.border_color);
+    div().pt_3().pb_3().child(rule).into_any()
 }