Add initial markdown preview to Zed (#6958)
Adds a "markdown: open preview" action to open a markdown preview. https://github.com/zed-industries/zed/assets/18583882/6fd7f009-53f7-4f98-84ea-7dd3f0dd11bf This PR extends the work done in `crates/rich_text` to render markdown to also support: - Variable heading sizes - Markdown tables - Code blocks - Block quotes ## Release Notes - Added `Markdown: Open preview` action to partially close ([#6789](https://github.com/zed-industries/zed/issues/6789)). ## Known issues that will not be included in this PR - Images. - Nested block quotes. - Footnote Reference. - Headers highlighting. - Inline code highlighting (this will need to be implemented in `rich_text`) - Checkboxes (`- [ ]` and `- [x]`) - Syntax highlighting in code blocks. - Markdown table text alignment. - Inner markdown URL clicks
This commit is contained in:
parent
3b882918f7
commit
8bafc61ef5
14 changed files with 547 additions and 8 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -4317,6 +4317,26 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown_preview"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"editor",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"menu",
|
||||||
|
"project",
|
||||||
|
"pulldown-cmark",
|
||||||
|
"rich_text",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -10315,6 +10335,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"markdown_preview",
|
||||||
"menu",
|
"menu",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
|
|
|
@ -42,6 +42,7 @@ members = [
|
||||||
"crates/live_kit_client",
|
"crates/live_kit_client",
|
||||||
"crates/live_kit_server",
|
"crates/live_kit_server",
|
||||||
"crates/lsp",
|
"crates/lsp",
|
||||||
|
"crates/markdown_preview",
|
||||||
"crates/media",
|
"crates/media",
|
||||||
"crates/menu",
|
"crates/menu",
|
||||||
"crates/multi_buffer",
|
"crates/multi_buffer",
|
||||||
|
@ -111,6 +112,7 @@ parking_lot = "0.11.1"
|
||||||
postage = { version = "0.5", features = ["futures-traits"] }
|
postage = { version = "0.5", features = ["futures-traits"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
prost = "0.8"
|
prost = "0.8"
|
||||||
|
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
refineable = { path = "./crates/refineable" }
|
refineable = { path = "./crates/refineable" }
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
|
|
|
@ -453,7 +453,7 @@ impl ChatPanel {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
|
rich_text::render_rich_text(message.body.clone(), &mentions, language_registry, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ async-trait.workspace = true
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
git = { path = "../git" }
|
git = { path = "../git" }
|
||||||
globset.workspace = true
|
globset.workspace = true
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
@ -38,7 +38,6 @@ log.workspace = true
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
|
||||||
rand = { workspace = true, optional = true }
|
rand = { workspace = true, optional = true }
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
@ -55,6 +54,7 @@ text = { path = "../text" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
tree-sitter-rust = { workspace = true, optional = true }
|
tree-sitter-rust = { workspace = true, optional = true }
|
||||||
tree-sitter-typescript = { workspace = true, optional = true }
|
tree-sitter-typescript = { workspace = true, optional = true }
|
||||||
|
pulldown-cmark.workspace = true
|
||||||
tree-sitter.workspace = true
|
tree-sitter.workspace = true
|
||||||
unicase = "2.6"
|
unicase = "2.6"
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
|
|
32
crates/markdown_preview/Cargo.toml
Normal file
32
crates/markdown_preview/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[package]
|
||||||
|
name = "markdown_preview"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/markdown_preview.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
language = { path = "../language" }
|
||||||
|
menu = { path = "../menu" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
rich_text = { path = "../rich_text" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
lazy_static.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
pulldown-cmark.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { path = "../editor", features = ["test-support"] }
|
1
crates/markdown_preview/LICENSE-GPL
Symbolic link
1
crates/markdown_preview/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
14
crates/markdown_preview/src/markdown_preview.rs
Normal file
14
crates/markdown_preview/src/markdown_preview.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use gpui::{actions, AppContext};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub mod markdown_preview_view;
|
||||||
|
pub mod markdown_renderer;
|
||||||
|
|
||||||
|
actions!(markdown, [OpenPreview]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(|workspace: &mut Workspace, cx| {
|
||||||
|
markdown_preview_view::MarkdownPreviewView::register(workspace, cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
134
crates/markdown_preview/src/markdown_preview_view.rs
Normal file
134
crates/markdown_preview/src/markdown_preview_view.rs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
use editor::{Editor, EditorEvent};
|
||||||
|
use gpui::{
|
||||||
|
canvas, AnyElement, AppContext, AvailableSpace, EventEmitter, FocusHandle, FocusableView,
|
||||||
|
InteractiveElement, IntoElement, ParentElement, Render, Styled, View, ViewContext,
|
||||||
|
};
|
||||||
|
use language::LanguageRegistry;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::prelude::*;
|
||||||
|
use workspace::item::Item;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::{markdown_renderer::render_markdown, OpenPreview};
|
||||||
|
|
||||||
|
pub struct MarkdownPreviewView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
languages: Arc<LanguageRegistry>,
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 view: View<MarkdownPreviewView> =
|
||||||
|
cx.new_view(|cx| MarkdownPreviewView::new(editor, languages, cx));
|
||||||
|
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
let editor = active_editor.read(cx);
|
||||||
|
let contents = editor.buffer().read(cx).snapshot(cx).text();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle,
|
||||||
|
languages,
|
||||||
|
contents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for MarkdownPreviewView {
|
||||||
|
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum PreviewEvent {}
|
||||||
|
|
||||||
|
impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
|
||||||
|
|
||||||
|
impl Item for MarkdownPreviewView {
|
||||||
|
type Event = PreviewEvent;
|
||||||
|
|
||||||
|
fn tab_content(
|
||||||
|
&self,
|
||||||
|
_detail: Option<usize>,
|
||||||
|
selected: bool,
|
||||||
|
_cx: &WindowContext,
|
||||||
|
) -> AnyElement {
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(Icon::new(IconName::FileDoc).color(if selected {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
}))
|
||||||
|
.child(Label::new("Markdown preview").color(if selected {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
}))
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("markdown preview")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for MarkdownPreviewView {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let rendered_markdown = v_flex()
|
||||||
|
.items_start()
|
||||||
|
.justify_start()
|
||||||
|
.key_context("MarkdownPreview")
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.id("MarkdownPreview")
|
||||||
|
.overflow_scroll()
|
||||||
|
.size_full()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.p_4()
|
||||||
|
.children(render_markdown(&self.contents, &self.languages, cx));
|
||||||
|
|
||||||
|
div().flex_1().child(
|
||||||
|
canvas(move |bounds, cx| {
|
||||||
|
rendered_markdown.into_any().draw(
|
||||||
|
bounds.origin,
|
||||||
|
bounds.size.map(AvailableSpace::Definite),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
328
crates/markdown_preview/src/markdown_renderer.rs
Normal file
328
crates/markdown_preview/src/markdown_renderer.rs
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
div, px, rems, AnyElement, DefiniteLength, Div, ElementId, Hsla, ParentElement, SharedString,
|
||||||
|
Styled, StyledText, WindowContext,
|
||||||
|
};
|
||||||
|
use language::LanguageRegistry;
|
||||||
|
use pulldown_cmark::{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 {
|
||||||
|
header: Vec<Div>,
|
||||||
|
body: Vec<Vec<Div>>,
|
||||||
|
current_row: Vec<Div>,
|
||||||
|
state: TableState,
|
||||||
|
border_color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkdownTable {
|
||||||
|
fn new(border_color: Hsla) -> Self {
|
||||||
|
Self {
|
||||||
|
header: Vec::new(),
|
||||||
|
body: Vec::new(),
|
||||||
|
current_row: Vec::new(),
|
||||||
|
state: TableState::Header,
|
||||||
|
border_color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 add_cell(&mut self, contents: AnyElement) {
|
||||||
|
let cell = div()
|
||||||
|
.child(contents)
|
||||||
|
.w_full()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Div {
|
||||||
|
let mut table = v_flex().w_full();
|
||||||
|
let mut header = h_flex();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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(_text_alignments) => {
|
||||||
|
self.table = Some(MarkdownTable::new(self.theme.colors().border));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 element = self.render_md_from_range(source_range.clone(), cx);
|
||||||
|
let paragraph = h_flex().mb_3().child(element);
|
||||||
|
|
||||||
|
self.finished.push(paragraph);
|
||||||
|
}
|
||||||
|
Tag::Heading(level, _, _) => {
|
||||||
|
let mut headline = self.headline(level);
|
||||||
|
if source_range.start > 0 {
|
||||||
|
headline = headline.mt_4();
|
||||||
|
}
|
||||||
|
|
||||||
|
let element = self.render_md_from_range(source_range.clone(), cx);
|
||||||
|
let headline = headline.child(element);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
self.finished.push(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.table.as_mut().unwrap().finish_row();
|
||||||
|
}
|
||||||
|
Tag::TableRow => {
|
||||||
|
if self.table.is_none() {
|
||||||
|
log::error!("Table row without table ({:?})", source_range);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.table.as_mut().unwrap().finish_row();
|
||||||
|
}
|
||||||
|
Tag::TableCell => {
|
||||||
|
if self.table.is_none() {
|
||||||
|
log::error!("Table cell without table ({:?})", source_range);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = self.render_md_from_range(source_range.clone(), cx);
|
||||||
|
self.table.as_mut().unwrap().add_cell(contents);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
|
let color = match level {
|
||||||
|
HeadingLevel::H6 => self.theme.colors().text_muted,
|
||||||
|
_ => self.theme.colors().text,
|
||||||
|
};
|
||||||
|
|
||||||
|
let line_height = DefiniteLength::from(rems(1.25));
|
||||||
|
|
||||||
|
let headline = h_flex()
|
||||||
|
.w_full()
|
||||||
|
.line_height(line_height)
|
||||||
|
.text_size(size)
|
||||||
|
.text_color(color)
|
||||||
|
.mb_4()
|
||||||
|
.pb(rems(0.15));
|
||||||
|
|
||||||
|
headline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ lsp = { path = "../lsp" }
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
pulldown-cmark.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
rich_text = { path = "../rich_text" }
|
rich_text = { path = "../rich_text" }
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
|
|
@ -22,7 +22,7 @@ futures.workspace = true
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
pulldown-cmark.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
|
|
@ -47,7 +47,7 @@ pub struct Mention {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RichText {
|
impl RichText {
|
||||||
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
|
pub fn element(&self, id: ElementId, cx: &WindowContext) -> AnyElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let code_background = theme.colors().surface_background;
|
let code_background = theme.colors().surface_background;
|
||||||
|
|
||||||
|
@ -83,7 +83,12 @@ impl RichText {
|
||||||
)
|
)
|
||||||
.on_click(self.link_ranges.clone(), {
|
.on_click(self.link_ranges.clone(), {
|
||||||
let link_urls = self.link_urls.clone();
|
let link_urls = self.link_urls.clone();
|
||||||
move |ix, cx| cx.open_url(&link_urls[ix])
|
move |ix, cx| {
|
||||||
|
let url = &link_urls[ix];
|
||||||
|
if url.starts_with("http") {
|
||||||
|
cx.open_url(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.tooltip({
|
.tooltip({
|
||||||
let link_ranges = self.link_ranges.clone();
|
let link_ranges = self.link_ranges.clone();
|
||||||
|
@ -256,7 +261,7 @@ pub fn render_markdown_mut(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_markdown(
|
pub fn render_rich_text(
|
||||||
block: String,
|
block: String,
|
||||||
mentions: &[Mention],
|
mentions: &[Mention],
|
||||||
language_registry: &Arc<LanguageRegistry>,
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
|
|
@ -65,6 +65,7 @@ lazy_static.workspace = true
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
markdown_preview = { path = "../markdown_preview" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
node_runtime = { path = "../node_runtime" }
|
node_runtime = { path = "../node_runtime" }
|
||||||
|
|
|
@ -248,6 +248,7 @@ fn main() {
|
||||||
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
collab_ui::init(&app_state, cx);
|
collab_ui::init(&app_state, cx);
|
||||||
feedback::init(cx);
|
feedback::init(cx);
|
||||||
|
markdown_preview::init(cx);
|
||||||
welcome::init(cx);
|
welcome::init(cx);
|
||||||
|
|
||||||
cx.set_menus(app_menus());
|
cx.set_menus(app_menus());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue