mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
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
21
Cargo.lock
generated
21
Cargo.lock
generated
@ -4317,6 +4317,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
@ -10315,6 +10335,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"mimalloc",
|
||||
"node_runtime",
|
||||
|
@ -42,6 +42,7 @@ members = [
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/multi_buffer",
|
||||
@ -111,6 +112,7 @@ parking_lot = "0.11.1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
prost = "0.8"
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
|
@ -453,7 +453,7 @@ impl ChatPanel {
|
||||
})
|
||||
.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>) {
|
||||
|
@ -29,7 +29,7 @@ async-trait.workspace = true
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
futures.workspace = true
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
globset.workspace = true
|
||||
gpui = { path = "../gpui" }
|
||||
@ -38,7 +38,6 @@ log.workspace = true
|
||||
lsp = { path = "../lsp" }
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
rpc = { path = "../rpc" }
|
||||
@ -55,6 +54,7 @@ text = { path = "../text" }
|
||||
theme = { path = "../theme" }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
pulldown-cmark.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
unicase = "2.6"
|
||||
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
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
pulldown-cmark.workspace = true
|
||||
rand.workspace = true
|
||||
rich_text = { path = "../rich_text" }
|
||||
schemars.workspace = true
|
||||
|
@ -22,7 +22,7 @@ futures.workspace = true
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lazy_static.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
pulldown-cmark.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
@ -47,7 +47,7 @@ pub struct Mention {
|
||||
}
|
||||
|
||||
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 code_background = theme.colors().surface_background;
|
||||
|
||||
@ -83,7 +83,12 @@ impl RichText {
|
||||
)
|
||||
.on_click(self.link_ranges.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({
|
||||
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,
|
||||
mentions: &[Mention],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
|
@ -65,6 +65,7 @@ lazy_static.workspace = true
|
||||
libc = "0.2"
|
||||
log.workspace = true
|
||||
lsp = { path = "../lsp" }
|
||||
markdown_preview = { path = "../markdown_preview" }
|
||||
menu = { path = "../menu" }
|
||||
mimalloc = "0.1"
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
|
@ -248,6 +248,7 @@ fn main() {
|
||||
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
collab_ui::init(&app_state, cx);
|
||||
feedback::init(cx);
|
||||
markdown_preview::init(cx);
|
||||
welcome::init(cx);
|
||||
|
||||
cx.set_menus(app_menus());
|
||||
|
Loading…
Reference in New Issue
Block a user