Introduce a new markdown
crate (#11556)
This pull request introduces a new `markdown` crate which is capable of parsing and rendering a Markdown source. One of the key additions is that it enables text selection within a `Markdown` view. Eventually, this will replace `RichText` but for now the goal is to use it in the assistant revamped assistant in the spirit of making progress. <img width="711" alt="image" src="https://github.com/zed-industries/zed/assets/482957/b56c777b-e57c-42f9-95c1-3ada22f63a69"> Note that this pull request doesn't yet use the new markdown renderer in `assistant2`. This is because we need to modify the assistant before slotting in the new renderer and I wanted to merge this independently of those changes. Release Notes: - N/A --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Conrad <conrad@zed.dev> Co-authored-by: Alp <akeles@umd.edu> Co-authored-by: Zachiah Sawyer <zachiah@proton.me>
This commit is contained in:
parent
ddaaaee973
commit
5df1481297
24 changed files with 1629 additions and 88 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -5974,6 +5974,27 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"env_logger",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"language",
|
||||
"languages",
|
||||
"linkify",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"pulldown-cmark",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown_preview"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -52,6 +52,7 @@ members = [
|
|||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/markdown",
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
|
@ -192,6 +193,7 @@ languages = { path = "crates/languages" }
|
|||
live_kit_client = { path = "crates/live_kit_client" }
|
||||
live_kit_server = { path = "crates/live_kit_server" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
media = { path = "crates/media" }
|
||||
menu = { path = "crates/menu" }
|
||||
|
|
|
@ -26,7 +26,7 @@ impl CollabNotification {
|
|||
}
|
||||
|
||||
impl ParentElement for CollabNotification {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ impl ExtensionCard {
|
|||
}
|
||||
|
||||
impl ParentElement for ExtensionCard {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ pub trait RenderOnce: 'static {
|
|||
/// can accept any number of any kind of child elements
|
||||
pub trait ParentElement {
|
||||
/// Extend this element's children with the given child elements.
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>);
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>);
|
||||
|
||||
/// Add a single child element to this element.
|
||||
fn child(mut self, child: impl IntoElement) -> Self
|
||||
|
|
|
@ -63,7 +63,7 @@ impl Anchored {
|
|||
}
|
||||
|
||||
impl ParentElement for Anchored {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2337,7 +2337,7 @@ impl<E> ParentElement for Focusable<E>
|
|||
where
|
||||
E: ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
|
@ -2430,7 +2430,7 @@ impl<E> ParentElement for Stateful<E>
|
|||
where
|
||||
E: ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ use std::{
|
|||
use util::ResultExt;
|
||||
|
||||
impl Element for &'static str {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
|
@ -29,7 +29,7 @@ impl Element for &'static str {
|
|||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let mut state = TextLayout::default();
|
||||
let layout_id = state.layout(SharedString::from(*self), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
@ -37,21 +37,22 @@ impl Element for &'static str {
|
|||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::RequestLayoutState,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
text_layout.prepaint(bounds, self)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut TextState,
|
||||
_bounds: Bounds<Pixels>,
|
||||
text_layout: &mut TextLayout,
|
||||
_: &mut (),
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
text_state.paint(bounds, self, cx)
|
||||
text_layout.paint(self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,7 +73,7 @@ impl IntoElement for String {
|
|||
}
|
||||
|
||||
impl Element for SharedString {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
|
@ -86,7 +87,7 @@ impl Element for SharedString {
|
|||
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let mut state = TextLayout::default();
|
||||
let layout_id = state.layout(self.clone(), None, cx);
|
||||
(layout_id, state)
|
||||
}
|
||||
|
@ -94,22 +95,22 @@ impl Element for SharedString {
|
|||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_text_state: &mut Self::RequestLayoutState,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
text_layout.prepaint(bounds, self.as_ref())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let text_str: &str = self.as_ref();
|
||||
text_state.paint(bounds, text_str, cx)
|
||||
text_layout.paint(self.as_ref(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,6 +130,7 @@ impl IntoElement for SharedString {
|
|||
pub struct StyledText {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
layout: TextLayout,
|
||||
}
|
||||
|
||||
impl StyledText {
|
||||
|
@ -137,9 +139,15 @@ impl StyledText {
|
|||
StyledText {
|
||||
text: text.into(),
|
||||
runs: None,
|
||||
layout: TextLayout::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn layout(&self) -> &TextLayout {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
/// as any ranges of text that have had their style customized.
|
||||
pub fn with_highlights(
|
||||
|
@ -167,10 +175,16 @@ impl StyledText {
|
|||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for StyledText {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
|
@ -184,29 +198,29 @@ impl Element for StyledText {
|
|||
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut state = TextState::default();
|
||||
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, state)
|
||||
let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx);
|
||||
(layout_id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_state: &mut Self::RequestLayoutState,
|
||||
bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_cx: &mut WindowContext,
|
||||
) {
|
||||
self.layout.prepaint(bounds, &self.text)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
text_state.paint(bounds, &self.text, cx)
|
||||
self.layout.paint(&self.text, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,19 +232,20 @@ impl IntoElement for StyledText {
|
|||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// todo!()
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
|
||||
pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
|
||||
|
||||
struct TextStateInner {
|
||||
struct TextLayoutInner {
|
||||
lines: SmallVec<[WrappedLine; 1]>,
|
||||
line_height: Pixels,
|
||||
wrap_width: Option<Pixels>,
|
||||
size: Option<Size<Pixels>>,
|
||||
bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
impl TextState {
|
||||
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
|
||||
impl TextLayout {
|
||||
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
|
||||
self.0.lock()
|
||||
}
|
||||
|
||||
|
@ -265,11 +280,11 @@ impl TextState {
|
|||
None
|
||||
};
|
||||
|
||||
if let Some(text_state) = element_state.0.lock().as_ref() {
|
||||
if text_state.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
|
||||
if let Some(text_layout) = element_state.0.lock().as_ref() {
|
||||
if text_layout.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
||||
{
|
||||
return text_state.size.unwrap();
|
||||
return text_layout.size.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -283,11 +298,12 @@ impl TextState {
|
|||
)
|
||||
.log_err()
|
||||
else {
|
||||
element_state.lock().replace(TextStateInner {
|
||||
element_state.lock().replace(TextLayoutInner {
|
||||
lines: Default::default(),
|
||||
line_height,
|
||||
wrap_width,
|
||||
size: Some(Size::default()),
|
||||
bounds: None,
|
||||
});
|
||||
return Size::default();
|
||||
};
|
||||
|
@ -299,11 +315,12 @@ impl TextState {
|
|||
size.width = size.width.max(line_size.width).ceil();
|
||||
}
|
||||
|
||||
element_state.lock().replace(TextStateInner {
|
||||
element_state.lock().replace(TextLayoutInner {
|
||||
lines,
|
||||
line_height,
|
||||
wrap_width,
|
||||
size: Some(size),
|
||||
bounds: None,
|
||||
});
|
||||
|
||||
size
|
||||
|
@ -313,12 +330,25 @@ impl TextState {
|
|||
layout_id
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
|
||||
fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
|
||||
let mut element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||
.unwrap();
|
||||
element_state.bounds = Some(bounds);
|
||||
}
|
||||
|
||||
fn paint(&mut self, text: &str, cx: &mut WindowContext) {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
|
||||
.unwrap();
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
|
||||
.unwrap();
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
let mut line_origin = bounds.origin;
|
||||
|
@ -328,15 +358,19 @@ impl TextState {
|
|||
}
|
||||
}
|
||||
|
||||
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
|
||||
if !bounds.contains(&position) {
|
||||
return None;
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.expect("prepaint has not been performed");
|
||||
|
||||
if position.y < bounds.top() {
|
||||
return Err(0);
|
||||
}
|
||||
|
||||
let line_height = element_state.line_height;
|
||||
let mut line_origin = bounds.origin;
|
||||
|
@ -348,14 +382,56 @@ impl TextState {
|
|||
line_start_ix += line.len() + 1;
|
||||
} else {
|
||||
let position_within_line = position - line_origin;
|
||||
let index_within_line =
|
||||
line.index_for_position(position_within_line, line_height)?;
|
||||
return Some(line_start_ix + index_within_line);
|
||||
match line.index_for_position(position_within_line, line_height) {
|
||||
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
|
||||
Err(index_within_line) => return Err(line_start_ix + index_within_line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(line_start_ix.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
|
||||
let element_state = self.lock();
|
||||
let element_state = element_state
|
||||
.as_ref()
|
||||
.expect("measurement has not been performed");
|
||||
let bounds = element_state
|
||||
.bounds
|
||||
.expect("prepaint has not been performed");
|
||||
let line_height = element_state.line_height;
|
||||
|
||||
let mut line_origin = bounds.origin;
|
||||
let mut line_start_ix = 0;
|
||||
|
||||
for line in &element_state.lines {
|
||||
let line_end_ix = line_start_ix + line.len();
|
||||
if index < line_start_ix {
|
||||
break;
|
||||
} else if index > line_end_ix {
|
||||
line_origin.y += line.size(line_height).height;
|
||||
line_start_ix = line_end_ix + 1;
|
||||
continue;
|
||||
} else {
|
||||
let ix_within_line = index - line_start_ix;
|
||||
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.lock().as_ref().unwrap().bounds.unwrap()
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn line_height(&self) -> Pixels {
|
||||
self.0.lock().as_ref().unwrap().line_height
|
||||
}
|
||||
}
|
||||
|
||||
/// A text element that can be interacted with.
|
||||
|
@ -436,7 +512,7 @@ impl InteractiveText {
|
|||
}
|
||||
|
||||
impl Element for InteractiveText {
|
||||
type RequestLayoutState = TextState;
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
|
@ -484,17 +560,18 @@ impl Element for InteractiveText {
|
|||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_state: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Hitbox,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let text_layout = self.text.layout().clone();
|
||||
cx.with_element_state::<InteractiveTextState, _>(
|
||||
global_id.unwrap(),
|
||||
|interactive_state, cx| {
|
||||
let mut interactive_state = interactive_state.unwrap_or_default();
|
||||
if let Some(click_listener) = self.click_listener.take() {
|
||||
let mouse_position = cx.mouse_position();
|
||||
if let Some(ix) = text_state.index_for_position(bounds, mouse_position) {
|
||||
if let Some(ix) = text_layout.index_for_position(mouse_position).ok() {
|
||||
if self
|
||||
.clickable_ranges
|
||||
.iter()
|
||||
|
@ -504,7 +581,7 @@ impl Element for InteractiveText {
|
|||
}
|
||||
}
|
||||
|
||||
let text_state = text_state.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
let mouse_down = interactive_state.mouse_down_index.clone();
|
||||
if let Some(mouse_down_index) = mouse_down.get() {
|
||||
let hitbox = hitbox.clone();
|
||||
|
@ -512,7 +589,7 @@ impl Element for InteractiveText {
|
|||
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
if let Some(mouse_up_index) =
|
||||
text_state.index_for_position(bounds, event.position)
|
||||
text_layout.index_for_position(event.position).ok()
|
||||
{
|
||||
click_listener(
|
||||
&clickable_ranges,
|
||||
|
@ -533,7 +610,7 @@ impl Element for InteractiveText {
|
|||
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
if let Some(mouse_down_index) =
|
||||
text_state.index_for_position(bounds, event.position)
|
||||
text_layout.index_for_position(event.position).ok()
|
||||
{
|
||||
mouse_down.set(Some(mouse_down_index));
|
||||
cx.refresh();
|
||||
|
@ -546,12 +623,12 @@ impl Element for InteractiveText {
|
|||
cx.on_mouse_event({
|
||||
let mut hover_listener = self.hover_listener.take();
|
||||
let hitbox = hitbox.clone();
|
||||
let text_state = text_state.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
let hovered_index = interactive_state.hovered_index.clone();
|
||||
move |event: &MouseMoveEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
|
||||
let current = hovered_index.get();
|
||||
let updated = text_state.index_for_position(bounds, event.position);
|
||||
let updated = text_layout.index_for_position(event.position).ok();
|
||||
if current != updated {
|
||||
hovered_index.set(updated);
|
||||
if let Some(hover_listener) = hover_listener.as_ref() {
|
||||
|
@ -567,10 +644,10 @@ impl Element for InteractiveText {
|
|||
let hitbox = hitbox.clone();
|
||||
let active_tooltip = interactive_state.active_tooltip.clone();
|
||||
let pending_mouse_down = interactive_state.mouse_down_index.clone();
|
||||
let text_state = text_state.clone();
|
||||
let text_layout = text_layout.clone();
|
||||
|
||||
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
|
||||
let position = text_state.index_for_position(bounds, event.position);
|
||||
let position = text_layout.index_for_position(event.position).ok();
|
||||
let is_hovered = position.is_some()
|
||||
&& hitbox.is_hovered(cx)
|
||||
&& pending_mouse_down.get().is_none();
|
||||
|
@ -621,7 +698,7 @@ impl Element for InteractiveText {
|
|||
});
|
||||
}
|
||||
|
||||
self.text.paint(None, bounds, text_state, &mut (), cx);
|
||||
self.text.paint(None, bounds, &mut (), &mut (), cx);
|
||||
|
||||
((), interactive_state)
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
|
||||
use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
|
||||
use collections::FxHashMap;
|
||||
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -254,39 +254,83 @@ impl WrappedLineLayout {
|
|||
/// The index corresponding to a given position in this layout for the given line height.
|
||||
pub fn index_for_position(
|
||||
&self,
|
||||
position: Point<Pixels>,
|
||||
mut position: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
) -> Option<usize> {
|
||||
) -> Result<usize, usize> {
|
||||
let wrapped_line_ix = (position.y / line_height) as usize;
|
||||
|
||||
let wrapped_line_start_x = if wrapped_line_ix > 0 {
|
||||
let wrapped_line_start_index;
|
||||
let wrapped_line_start_x;
|
||||
if wrapped_line_ix > 0 {
|
||||
let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
|
||||
return None;
|
||||
return Err(0);
|
||||
};
|
||||
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
|
||||
run.glyphs[line_start_boundary.glyph_ix].position.x
|
||||
let glyph = &run.glyphs[line_start_boundary.glyph_ix];
|
||||
wrapped_line_start_index = glyph.index;
|
||||
wrapped_line_start_x = glyph.position.x;
|
||||
} else {
|
||||
Pixels::ZERO
|
||||
wrapped_line_start_index = 0;
|
||||
wrapped_line_start_x = Pixels::ZERO;
|
||||
};
|
||||
|
||||
let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() {
|
||||
let wrapped_line_end_index;
|
||||
let wrapped_line_end_x;
|
||||
if wrapped_line_ix < self.wrap_boundaries.len() {
|
||||
let next_wrap_boundary_ix = wrapped_line_ix;
|
||||
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
|
||||
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
|
||||
run.glyphs[next_wrap_boundary.glyph_ix].position.x
|
||||
let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
|
||||
wrapped_line_end_index = glyph.index;
|
||||
wrapped_line_end_x = glyph.position.x;
|
||||
} else {
|
||||
self.unwrapped_layout.width
|
||||
wrapped_line_end_index = self.unwrapped_layout.len;
|
||||
wrapped_line_end_x = self.unwrapped_layout.width;
|
||||
};
|
||||
|
||||
let mut position_in_unwrapped_line = position;
|
||||
position_in_unwrapped_line.x += wrapped_line_start_x;
|
||||
if position_in_unwrapped_line.x > wrapped_line_end_x {
|
||||
None
|
||||
if position_in_unwrapped_line.x < wrapped_line_start_x {
|
||||
Err(wrapped_line_start_index)
|
||||
} else if position_in_unwrapped_line.x >= wrapped_line_end_x {
|
||||
Err(wrapped_line_end_index)
|
||||
} else {
|
||||
self.unwrapped_layout
|
||||
Ok(self
|
||||
.unwrapped_layout
|
||||
.index_for_x(position_in_unwrapped_line.x)
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// todo!()
|
||||
pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
|
||||
let mut line_start_ix = 0;
|
||||
let mut line_end_indices = self
|
||||
.wrap_boundaries
|
||||
.iter()
|
||||
.map(|wrap_boundary| {
|
||||
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
|
||||
let glyph = &run.glyphs[wrap_boundary.glyph_ix];
|
||||
glyph.index
|
||||
})
|
||||
.chain([self.len()])
|
||||
.enumerate();
|
||||
for (ix, line_end_ix) in line_end_indices {
|
||||
let line_y = ix as f32 * line_height;
|
||||
if index < line_start_ix {
|
||||
break;
|
||||
} else if index > line_end_ix {
|
||||
line_start_ix = line_end_ix;
|
||||
continue;
|
||||
} else {
|
||||
let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
|
||||
let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
|
||||
return Some(point(x, line_y));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LineLayoutCache {
|
||||
|
|
40
crates/markdown/Cargo.toml
Normal file
40
crates/markdown/Cargo.toml
Normal file
|
@ -0,0 +1,40 @@
|
|||
[package]
|
||||
name = "markdown"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/markdown.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
pulldown-cmark.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
languages.workspace = true
|
||||
node_runtime.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
181
crates/markdown/examples/markdown.rs
Normal file
181
crates/markdown/examples/markdown.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
use assets::Assets;
|
||||
use gpui::{prelude::*, App, Task, View, WindowOptions};
|
||||
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use theme::LoadThemes;
|
||||
use ui::prelude::*;
|
||||
use ui::{div, WindowContext};
|
||||
|
||||
const MARKDOWN_EXAMPLE: &'static str = r#"
|
||||
# Markdown Example Document
|
||||
|
||||
## Headings
|
||||
Headings are created by adding one or more `#` symbols before your heading text. The number of `#` you use will determine the size of the heading.
|
||||
|
||||
## Emphasis
|
||||
Emphasis can be added with italics or bold. *This text will be italic*. _This will also be italic_
|
||||
|
||||
## Lists
|
||||
|
||||
### Unordered Lists
|
||||
Unordered lists use asterisks `*`, plus `+`, or minus `-` as list markers.
|
||||
|
||||
* Item 1
|
||||
* Item 2
|
||||
* Item 2a
|
||||
* Item 2b
|
||||
|
||||
### Ordered Lists
|
||||
Ordered lists use numbers followed by a period.
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
3. Item 3
|
||||
1. Item 3a
|
||||
2. Item 3b
|
||||
|
||||
## Links
|
||||
Links are created using the format [http://zed.dev](https://zed.dev).
|
||||
|
||||
They can also be detected automatically, for example https://zed.dev/blog.
|
||||
|
||||
## Images
|
||||
Images are like links, but with an exclamation mark `!` in front.
|
||||
|
||||
```todo!
|
||||

|
||||
```
|
||||
|
||||
## Code
|
||||
Inline `code` can be wrapped with backticks `` ` ``.
|
||||
|
||||
```markdown
|
||||
Inline `code` has `back-ticks around` it.
|
||||
```
|
||||
|
||||
Code blocks can be created by indenting lines by four spaces or with triple backticks ```.
|
||||
|
||||
```javascript
|
||||
function test() {
|
||||
console.log("notice the blank line before this function?");
|
||||
}
|
||||
```
|
||||
|
||||
## Blockquotes
|
||||
Blockquotes are created with `>`.
|
||||
|
||||
> This is a blockquote.
|
||||
|
||||
## Horizontal Rules
|
||||
Horizontal rules are created using three or more asterisks `***`, dashes `---`, or underscores `___`.
|
||||
|
||||
## Line breaks
|
||||
This is a
|
||||
\
|
||||
line break!
|
||||
|
||||
---
|
||||
|
||||
Remember, markdown processors may have slight differences and extensions, so always refer to the specific documentation or guides relevant to your platform or editor for the best practices and additional features.
|
||||
"#;
|
||||
|
||||
pub fn main() {
|
||||
env_logger::init();
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
language::init(cx);
|
||||
SettingsStore::update(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| {
|
||||
MarkdownExample::new(
|
||||
MARKDOWN_EXAMPLE.to_string(),
|
||||
MarkdownStyle {
|
||||
code_block: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
// @nate: Could we add inline-code specific styles to the theme?
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: {
|
||||
let mut selection = cx.theme().players().local().selection;
|
||||
selection.fade_out(0.7);
|
||||
selection
|
||||
},
|
||||
},
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
struct MarkdownExample {
|
||||
markdown: View<Markdown>,
|
||||
}
|
||||
|
||||
impl MarkdownExample {
|
||||
pub fn new(
|
||||
text: String,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let markdown = cx.new_view(|cx| Markdown::new(text, style, language_registry, cx));
|
||||
Self { markdown }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MarkdownExample {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("markdown-example")
|
||||
.debug_selector(|| "foo".into())
|
||||
.relative()
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
.overflow_y_scroll()
|
||||
.child(self.markdown.clone())
|
||||
}
|
||||
}
|
902
crates/markdown/src/markdown.rs
Normal file
902
crates/markdown/src/markdown.rs
Normal file
|
@ -0,0 +1,902 @@
|
|||
mod parser;
|
||||
|
||||
use crate::parser::CodeBlockKind;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
point, quad, AnyElement, Bounds, CursorStyle, DispatchPhase, Edges, FontStyle, FontWeight,
|
||||
GlobalElementId, Hitbox, Hsla, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
|
||||
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
|
||||
TextStyleRefinement, View,
|
||||
};
|
||||
use language::{Language, LanguageRegistry, Rope};
|
||||
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
|
||||
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MarkdownStyle {
|
||||
pub code_block: TextStyleRefinement,
|
||||
pub inline_code: TextStyleRefinement,
|
||||
pub block_quote: TextStyleRefinement,
|
||||
pub link: TextStyleRefinement,
|
||||
pub rule_color: Hsla,
|
||||
pub block_quote_border_color: Hsla,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub selection_background_color: Hsla,
|
||||
}
|
||||
|
||||
pub struct Markdown {
|
||||
source: String,
|
||||
selection: Selection,
|
||||
pressed_link: Option<RenderedLink>,
|
||||
autoscroll_request: Option<usize>,
|
||||
style: MarkdownStyle,
|
||||
parsed_markdown: ParsedMarkdown,
|
||||
should_reparse: bool,
|
||||
pending_parse: Option<Task<Option<()>>>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl Markdown {
|
||||
pub fn new(
|
||||
source: String,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
source,
|
||||
selection: Selection::default(),
|
||||
pressed_link: None,
|
||||
autoscroll_request: None,
|
||||
style,
|
||||
should_reparse: false,
|
||||
parsed_markdown: ParsedMarkdown::default(),
|
||||
pending_parse: None,
|
||||
language_registry,
|
||||
};
|
||||
this.parse(cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
self.source.push_str(text);
|
||||
self.parse(cx);
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &str {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn parse(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.source.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.pending_parse.is_some() {
|
||||
self.should_reparse = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.source.clone();
|
||||
let parsed = cx.background_executor().spawn(async move {
|
||||
let text = SharedString::from(text);
|
||||
let events = Arc::from(parse_markdown(text.as_ref()));
|
||||
anyhow::Ok(ParsedMarkdown {
|
||||
source: text,
|
||||
events,
|
||||
})
|
||||
});
|
||||
|
||||
self.should_reparse = false;
|
||||
self.pending_parse = Some(cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let parsed = parsed.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.parsed_markdown = parsed;
|
||||
this.pending_parse.take();
|
||||
if this.should_reparse {
|
||||
this.parse(cx);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Markdown {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
MarkdownElement::new(
|
||||
cx.view().clone(),
|
||||
self.style.clone(),
|
||||
self.language_registry.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug)]
|
||||
struct Selection {
|
||||
start: usize,
|
||||
end: usize,
|
||||
reversed: bool,
|
||||
pending: bool,
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
fn set_head(&mut self, head: usize) {
|
||||
if head < self.tail() {
|
||||
if !self.reversed {
|
||||
self.end = self.start;
|
||||
self.reversed = true;
|
||||
}
|
||||
self.start = head;
|
||||
} else {
|
||||
if self.reversed {
|
||||
self.start = self.end;
|
||||
self.reversed = false;
|
||||
}
|
||||
self.end = head;
|
||||
}
|
||||
}
|
||||
|
||||
fn tail(&self) -> usize {
|
||||
if self.reversed {
|
||||
self.end
|
||||
} else {
|
||||
self.start
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ParsedMarkdown {
|
||||
source: SharedString,
|
||||
events: Arc<[(Range<usize>, MarkdownEvent)]>,
|
||||
}
|
||||
|
||||
impl Default for ParsedMarkdown {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
source: SharedString::default(),
|
||||
events: Arc::from([]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MarkdownElement {
|
||||
markdown: View<Markdown>,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl MarkdownElement {
|
||||
fn new(
|
||||
markdown: View<Markdown>,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
markdown,
|
||||
style,
|
||||
language_registry,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
|
||||
let language = self
|
||||
.language_registry
|
||||
.language_for_name(name)
|
||||
.map(|language| language.ok())
|
||||
.shared();
|
||||
|
||||
match language.clone().now_or_never() {
|
||||
Some(language) => language,
|
||||
None => {
|
||||
let markdown = self.markdown.downgrade();
|
||||
cx.spawn(|mut cx| async move {
|
||||
language.await;
|
||||
markdown.update(&mut cx, |_, cx| cx.notify())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_selection(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
rendered_text: &RenderedText,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let selection = self.markdown.read(cx).selection;
|
||||
let selection_start = rendered_text.position_for_source_index(selection.start);
|
||||
let selection_end = rendered_text.position_for_source_index(selection.end);
|
||||
|
||||
if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
|
||||
selection_start.zip(selection_end)
|
||||
{
|
||||
if start_position.y == end_position.y {
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
start_position,
|
||||
point(end_position.x, end_position.y + end_line_height),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
} else {
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
start_position,
|
||||
point(bounds.right(), start_position.y + start_line_height),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
|
||||
if end_position.y > start_position.y + start_line_height {
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
point(bounds.left(), start_position.y + start_line_height),
|
||||
point(bounds.right(), end_position.y),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
}
|
||||
|
||||
cx.paint_quad(quad(
|
||||
Bounds::from_corners(
|
||||
point(bounds.left(), end_position.y),
|
||||
point(end_position.x, end_position.y + end_line_height),
|
||||
),
|
||||
Pixels::ZERO,
|
||||
self.style.selection_background_color,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_mouse_listeners(
|
||||
&mut self,
|
||||
hitbox: &Hitbox,
|
||||
rendered_text: &RenderedText,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let is_hovering_link = hitbox.is_hovered(cx)
|
||||
&& !self.markdown.read(cx).selection.pending
|
||||
&& rendered_text
|
||||
.link_for_position(cx.mouse_position())
|
||||
.is_some();
|
||||
|
||||
if is_hovering_link {
|
||||
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
} else {
|
||||
cx.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
|
||||
self.on_mouse_event(cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
move |markdown, event: &MouseDownEvent, phase, cx| {
|
||||
if hitbox.is_hovered(cx) {
|
||||
if phase.bubble() {
|
||||
if let Some(link) = rendered_text.link_for_position(event.position) {
|
||||
markdown.pressed_link = Some(link.clone());
|
||||
} else {
|
||||
let source_index =
|
||||
match rendered_text.source_index_for_position(event.position) {
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
markdown.selection = Selection {
|
||||
start: source_index,
|
||||
end: source_index,
|
||||
reversed: false,
|
||||
pending: true,
|
||||
};
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
} else if phase.capture() {
|
||||
markdown.selection = Selection::default();
|
||||
markdown.pressed_link = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
self.on_mouse_event(cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovering_link = is_hovering_link;
|
||||
move |markdown, event: &MouseMoveEvent, phase, cx| {
|
||||
if phase.capture() {
|
||||
return;
|
||||
}
|
||||
|
||||
if markdown.selection.pending {
|
||||
let source_index = match rendered_text.source_index_for_position(event.position)
|
||||
{
|
||||
Ok(ix) | Err(ix) => ix,
|
||||
};
|
||||
markdown.selection.set_head(source_index);
|
||||
markdown.autoscroll_request = Some(source_index);
|
||||
cx.notify();
|
||||
} else {
|
||||
let is_hovering_link = hitbox.is_hovered(cx)
|
||||
&& rendered_text.link_for_position(event.position).is_some();
|
||||
if is_hovering_link != was_hovering_link {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.on_mouse_event(cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
move |markdown, event: &MouseUpEvent, phase, cx| {
|
||||
if phase.bubble() {
|
||||
if let Some(pressed_link) = markdown.pressed_link.take() {
|
||||
if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
|
||||
cx.open_url(&pressed_link.destination_url);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if markdown.selection.pending {
|
||||
markdown.selection.pending = false;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> {
|
||||
let autoscroll_index = self
|
||||
.markdown
|
||||
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;
|
||||
let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
|
||||
|
||||
let text_style = cx.text_style();
|
||||
let font_id = cx.text_system().resolve_font(&text_style.font());
|
||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||
let em_width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, font_size, 'm')
|
||||
.unwrap()
|
||||
.size
|
||||
.width;
|
||||
cx.request_autoscroll(Bounds::from_corners(
|
||||
point(position.x - 3. * em_width, position.y - 3. * line_height),
|
||||
point(position.x + 3. * em_width, position.y + 3. * line_height),
|
||||
));
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn on_mouse_event<T: MouseEvent>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
mut f: impl 'static + FnMut(&mut Markdown, &T, DispatchPhase, &mut ViewContext<Markdown>),
|
||||
) {
|
||||
cx.on_mouse_event({
|
||||
let markdown = self.markdown.downgrade();
|
||||
move |event, phase, cx| {
|
||||
markdown
|
||||
.update(cx, |markdown, cx| f(markdown, event, phase, cx))
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for MarkdownElement {
|
||||
type RequestLayoutState = RenderedMarkdown;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
|
||||
let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
|
||||
for (range, event) in parsed_markdown.events.iter() {
|
||||
match event {
|
||||
MarkdownEvent::Start(tag) => {
|
||||
match tag {
|
||||
MarkdownTag::Paragraph => {
|
||||
builder.push_div(div().mb_2().line_height(rems(1.3)));
|
||||
}
|
||||
MarkdownTag::Heading { level, .. } => {
|
||||
let mut heading = div().mb_2();
|
||||
heading = match level {
|
||||
pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
|
||||
pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
|
||||
pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
|
||||
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
|
||||
_ => heading,
|
||||
};
|
||||
builder.push_div(heading);
|
||||
}
|
||||
MarkdownTag::BlockQuote => {
|
||||
builder.push_text_style(self.style.block_quote.clone());
|
||||
builder.push_div(
|
||||
div()
|
||||
.pl_4()
|
||||
.mb_2()
|
||||
.border_l_4()
|
||||
.border_color(self.style.block_quote_border_color),
|
||||
);
|
||||
}
|
||||
MarkdownTag::CodeBlock(kind) => {
|
||||
let language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
self.load_language(language.as_ref(), cx)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
builder.push_code_block(language);
|
||||
builder.push_text_style(self.style.code_block.clone());
|
||||
builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
|
||||
self.style.code_block.background_color,
|
||||
|div, color| div.bg(color),
|
||||
));
|
||||
}
|
||||
MarkdownTag::HtmlBlock => builder.push_div(div()),
|
||||
MarkdownTag::List(bullet_index) => {
|
||||
builder.push_list(*bullet_index);
|
||||
builder.push_div(div().pl_4());
|
||||
}
|
||||
MarkdownTag::Item => {
|
||||
let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
|
||||
format!("{}.", bullet_index)
|
||||
} else {
|
||||
"•".to_string()
|
||||
};
|
||||
builder.push_div(
|
||||
div()
|
||||
.h_flex()
|
||||
.mb_2()
|
||||
.line_height(rems(1.3))
|
||||
.items_start()
|
||||
.gap_1()
|
||||
.child(bullet),
|
||||
);
|
||||
// Without `w_0`, text doesn't wrap to the width of the container.
|
||||
builder.push_div(div().flex_1().w_0());
|
||||
}
|
||||
MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
}),
|
||||
MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}),
|
||||
MarkdownTag::Strikethrough => {
|
||||
builder.push_text_style(TextStyleRefinement {
|
||||
strikethrough: Some(StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
color: None,
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
MarkdownTag::Link { dest_url, .. } => {
|
||||
builder.push_link(dest_url.clone(), range.clone());
|
||||
builder.push_text_style(self.style.link.clone())
|
||||
}
|
||||
_ => log::error!("unsupported markdown tag {:?}", tag),
|
||||
}
|
||||
}
|
||||
MarkdownEvent::End(tag) => match tag {
|
||||
MarkdownTagEnd::Paragraph => {
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Heading(_) => builder.pop_div(),
|
||||
MarkdownTagEnd::BlockQuote => {
|
||||
builder.pop_text_style();
|
||||
builder.pop_div()
|
||||
}
|
||||
MarkdownTagEnd::CodeBlock => {
|
||||
builder.trim_trailing_newline();
|
||||
builder.pop_div();
|
||||
builder.pop_text_style();
|
||||
builder.pop_code_block();
|
||||
}
|
||||
MarkdownTagEnd::HtmlBlock => builder.pop_div(),
|
||||
MarkdownTagEnd::List(_) => {
|
||||
builder.pop_list();
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Item => {
|
||||
builder.pop_div();
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Emphasis => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Strong => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
|
||||
MarkdownTagEnd::Link => builder.pop_text_style(),
|
||||
_ => log::error!("unsupported markdown tag end: {:?}", tag),
|
||||
},
|
||||
MarkdownEvent::Text => {
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
}
|
||||
MarkdownEvent::Code => {
|
||||
builder.push_text_style(self.style.inline_code.clone());
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
builder.pop_text_style();
|
||||
}
|
||||
MarkdownEvent::Html => {
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
}
|
||||
MarkdownEvent::InlineHtml => {
|
||||
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
|
||||
}
|
||||
MarkdownEvent::Rule => {
|
||||
builder.push_div(
|
||||
div()
|
||||
.border_b_1()
|
||||
.my_2()
|
||||
.border_color(self.style.rule_color),
|
||||
);
|
||||
builder.pop_div()
|
||||
}
|
||||
MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
|
||||
MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
|
||||
_ => log::error!("unsupported markdown event {:?}", event),
|
||||
}
|
||||
}
|
||||
|
||||
let mut rendered_markdown = builder.build();
|
||||
let child_layout_id = rendered_markdown.element.request_layout(cx);
|
||||
let layout_id = cx.request_layout(&Style::default(), [child_layout_id]);
|
||||
(layout_id, rendered_markdown)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
rendered_markdown: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
let hitbox = cx.insert_hitbox(bounds, false);
|
||||
rendered_markdown.element.prepaint(cx);
|
||||
self.autoscroll(&rendered_markdown.text, cx);
|
||||
hitbox
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
rendered_markdown: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx);
|
||||
rendered_markdown.element.paint(cx);
|
||||
self.paint_selection(bounds, &rendered_markdown.text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for MarkdownElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct MarkdownElementBuilder {
|
||||
div_stack: Vec<Div>,
|
||||
rendered_lines: Vec<RenderedLine>,
|
||||
pending_line: PendingLine,
|
||||
rendered_links: Vec<RenderedLink>,
|
||||
current_source_index: usize,
|
||||
base_text_style: TextStyle,
|
||||
text_style_stack: Vec<TextStyleRefinement>,
|
||||
code_block_stack: Vec<Option<Arc<Language>>>,
|
||||
list_stack: Vec<ListStackEntry>,
|
||||
syntax_theme: Arc<SyntaxTheme>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PendingLine {
|
||||
text: String,
|
||||
runs: Vec<TextRun>,
|
||||
source_mappings: Vec<SourceMapping>,
|
||||
}
|
||||
|
||||
struct ListStackEntry {
|
||||
bullet_index: Option<u64>,
|
||||
}
|
||||
|
||||
impl MarkdownElementBuilder {
|
||||
fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
|
||||
Self {
|
||||
div_stack: vec![div().debug_selector(|| "inner".into())],
|
||||
rendered_lines: Vec::new(),
|
||||
pending_line: PendingLine::default(),
|
||||
rendered_links: Vec::new(),
|
||||
current_source_index: 0,
|
||||
base_text_style,
|
||||
text_style_stack: Vec::new(),
|
||||
code_block_stack: Vec::new(),
|
||||
list_stack: Vec::new(),
|
||||
syntax_theme,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_text_style(&mut self, style: TextStyleRefinement) {
|
||||
self.text_style_stack.push(style);
|
||||
}
|
||||
|
||||
fn text_style(&self) -> TextStyle {
|
||||
let mut style = self.base_text_style.clone();
|
||||
for refinement in &self.text_style_stack {
|
||||
style.refine(refinement);
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
fn pop_text_style(&mut self) {
|
||||
self.text_style_stack.pop();
|
||||
}
|
||||
|
||||
fn push_div(&mut self, div: Div) {
|
||||
self.flush_text();
|
||||
self.div_stack.push(div);
|
||||
}
|
||||
|
||||
fn pop_div(&mut self) {
|
||||
self.flush_text();
|
||||
let div = self.div_stack.pop().unwrap().into_any();
|
||||
self.div_stack.last_mut().unwrap().extend(iter::once(div));
|
||||
}
|
||||
|
||||
fn push_list(&mut self, bullet_index: Option<u64>) {
|
||||
self.list_stack.push(ListStackEntry { bullet_index });
|
||||
}
|
||||
|
||||
fn next_bullet_index(&mut self) -> Option<u64> {
|
||||
self.list_stack.last_mut().and_then(|entry| {
|
||||
let item_index = entry.bullet_index.as_mut()?;
|
||||
*item_index += 1;
|
||||
Some(*item_index - 1)
|
||||
})
|
||||
}
|
||||
|
||||
fn pop_list(&mut self) {
|
||||
self.list_stack.pop();
|
||||
}
|
||||
|
||||
fn push_code_block(&mut self, language: Option<Arc<Language>>) {
|
||||
self.code_block_stack.push(language);
|
||||
}
|
||||
|
||||
fn pop_code_block(&mut self) {
|
||||
self.code_block_stack.pop();
|
||||
}
|
||||
|
||||
fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
|
||||
self.rendered_links.push(RenderedLink {
|
||||
source_range,
|
||||
destination_url,
|
||||
});
|
||||
}
|
||||
|
||||
fn push_text(&mut self, text: &str, source_index: usize) {
|
||||
self.pending_line.source_mappings.push(SourceMapping {
|
||||
rendered_index: self.pending_line.text.len(),
|
||||
source_index,
|
||||
});
|
||||
self.pending_line.text.push_str(text);
|
||||
self.current_source_index = source_index + text.len();
|
||||
|
||||
if let Some(Some(language)) = self.code_block_stack.last() {
|
||||
let mut offset = 0;
|
||||
for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
|
||||
if range.start > offset {
|
||||
self.pending_line
|
||||
.runs
|
||||
.push(self.text_style().to_run(range.start - offset));
|
||||
}
|
||||
|
||||
let mut run_style = self.text_style();
|
||||
if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
|
||||
run_style = run_style.highlight(highlight);
|
||||
}
|
||||
self.pending_line.runs.push(run_style.to_run(range.len()));
|
||||
offset = range.end;
|
||||
}
|
||||
|
||||
if offset < text.len() {
|
||||
self.pending_line
|
||||
.runs
|
||||
.push(self.text_style().to_run(text.len() - offset));
|
||||
}
|
||||
} else {
|
||||
self.pending_line
|
||||
.runs
|
||||
.push(self.text_style().to_run(text.len()));
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_trailing_newline(&mut self) {
|
||||
if self.pending_line.text.ends_with('\n') {
|
||||
self.pending_line
|
||||
.text
|
||||
.truncate(self.pending_line.text.len() - 1);
|
||||
self.pending_line.runs.last_mut().unwrap().len -= 1;
|
||||
self.current_source_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_text(&mut self) {
|
||||
let line = mem::take(&mut self.pending_line);
|
||||
if line.text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = StyledText::new(line.text).with_runs(line.runs);
|
||||
self.rendered_lines.push(RenderedLine {
|
||||
layout: text.layout().clone(),
|
||||
source_mappings: line.source_mappings,
|
||||
source_end: self.current_source_index,
|
||||
});
|
||||
self.div_stack.last_mut().unwrap().extend([text.into_any()]);
|
||||
}
|
||||
|
||||
fn build(mut self) -> RenderedMarkdown {
|
||||
debug_assert_eq!(self.div_stack.len(), 1);
|
||||
self.flush_text();
|
||||
RenderedMarkdown {
|
||||
element: self.div_stack.pop().unwrap().into_any(),
|
||||
text: RenderedText {
|
||||
lines: self.rendered_lines.into(),
|
||||
links: self.rendered_links.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderedLine {
|
||||
layout: TextLayout,
|
||||
source_mappings: Vec<SourceMapping>,
|
||||
source_end: usize,
|
||||
}
|
||||
|
||||
impl RenderedLine {
|
||||
fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
|
||||
let mapping = match self
|
||||
.source_mappings
|
||||
.binary_search_by_key(&source_index, |probe| probe.source_index)
|
||||
{
|
||||
Ok(ix) => &self.source_mappings[ix],
|
||||
Err(ix) => &self.source_mappings[ix - 1],
|
||||
};
|
||||
mapping.rendered_index + (source_index - mapping.source_index)
|
||||
}
|
||||
|
||||
fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
|
||||
let mapping = match self
|
||||
.source_mappings
|
||||
.binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
|
||||
{
|
||||
Ok(ix) => &self.source_mappings[ix],
|
||||
Err(ix) => &self.source_mappings[ix - 1],
|
||||
};
|
||||
mapping.source_index + (rendered_index - mapping.rendered_index)
|
||||
}
|
||||
|
||||
fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
|
||||
let line_rendered_index;
|
||||
let out_of_bounds;
|
||||
match self.layout.index_for_position(position) {
|
||||
Ok(ix) => {
|
||||
line_rendered_index = ix;
|
||||
out_of_bounds = false;
|
||||
}
|
||||
Err(ix) => {
|
||||
line_rendered_index = ix;
|
||||
out_of_bounds = true;
|
||||
}
|
||||
};
|
||||
let source_index = self.source_index_for_rendered_index(line_rendered_index);
|
||||
if out_of_bounds {
|
||||
Err(source_index)
|
||||
} else {
|
||||
Ok(source_index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
struct SourceMapping {
|
||||
rendered_index: usize,
|
||||
source_index: usize,
|
||||
}
|
||||
|
||||
pub struct RenderedMarkdown {
|
||||
element: AnyElement,
|
||||
text: RenderedText,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RenderedText {
|
||||
lines: Rc<[RenderedLine]>,
|
||||
links: Rc<[RenderedLink]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
struct RenderedLink {
|
||||
source_range: Range<usize>,
|
||||
destination_url: SharedString,
|
||||
}
|
||||
|
||||
impl RenderedText {
|
||||
fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
|
||||
let mut lines = self.lines.iter().peekable();
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
let line_bounds = line.layout.bounds();
|
||||
if position.y > line_bounds.bottom() {
|
||||
if let Some(next_line) = lines.peek() {
|
||||
if position.y < next_line.layout.bounds().top() {
|
||||
return Err(line.source_end);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return line.source_index_for_position(position);
|
||||
}
|
||||
|
||||
Err(self.lines.last().map_or(0, |line| line.source_end))
|
||||
}
|
||||
|
||||
fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
|
||||
for line in self.lines.iter() {
|
||||
let line_source_start = line.source_mappings.first().unwrap().source_index;
|
||||
if source_index < line_source_start {
|
||||
break;
|
||||
} else if source_index > line.source_end {
|
||||
continue;
|
||||
} else {
|
||||
let line_height = line.layout.line_height();
|
||||
let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
|
||||
let position = line.layout.position_for_index(rendered_index_within_line)?;
|
||||
return Some((position, line_height));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
|
||||
let source_index = self.source_index_for_position(position).ok()?;
|
||||
self.links
|
||||
.iter()
|
||||
.find(|link| link.source_range.contains(&source_index))
|
||||
}
|
||||
}
|
274
crates/markdown/src/parser.rs
Normal file
274
crates/markdown/src/parser.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
use gpui::SharedString;
|
||||
use linkify::LinkFinder;
|
||||
pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
|
||||
use pulldown_cmark::{Alignment, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser};
|
||||
use std::ops::Range;
|
||||
|
||||
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
||||
let mut events = Vec::new();
|
||||
let mut within_link = false;
|
||||
for (pulldown_event, mut range) in Parser::new_ext(text, Options::all()).into_offset_iter() {
|
||||
match pulldown_event {
|
||||
pulldown_cmark::Event::Start(tag) => {
|
||||
if let pulldown_cmark::Tag::Link { .. } = tag {
|
||||
within_link = true;
|
||||
}
|
||||
events.push((range, MarkdownEvent::Start(tag.into())))
|
||||
}
|
||||
pulldown_cmark::Event::End(tag) => {
|
||||
if let pulldown_cmark::TagEnd::Link = tag {
|
||||
within_link = false;
|
||||
}
|
||||
events.push((range, MarkdownEvent::End(tag)));
|
||||
}
|
||||
pulldown_cmark::Event::Text(_) => {
|
||||
// Automatically detect links in text if we're not already within a markdown
|
||||
// link.
|
||||
if !within_link {
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.kinds(&[linkify::LinkKind::Url]);
|
||||
let text_range = range.clone();
|
||||
for link in finder.links(&text[text_range.clone()]) {
|
||||
let link_range =
|
||||
text_range.start + link.start()..text_range.start + link.end();
|
||||
|
||||
if link_range.start > range.start {
|
||||
events.push((range.start..link_range.start, MarkdownEvent::Text));
|
||||
}
|
||||
|
||||
events.push((
|
||||
link_range.clone(),
|
||||
MarkdownEvent::Start(MarkdownTag::Link {
|
||||
link_type: LinkType::Autolink,
|
||||
dest_url: SharedString::from(link.as_str().to_string()),
|
||||
title: SharedString::default(),
|
||||
id: SharedString::default(),
|
||||
}),
|
||||
));
|
||||
events.push((link_range.clone(), MarkdownEvent::Text));
|
||||
events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link)));
|
||||
|
||||
range.start = link_range.end;
|
||||
}
|
||||
}
|
||||
|
||||
if range.start < range.end {
|
||||
events.push((range, MarkdownEvent::Text));
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Event::Code(_) => {
|
||||
range.start += 1;
|
||||
range.end -= 1;
|
||||
events.push((range, MarkdownEvent::Code))
|
||||
}
|
||||
pulldown_cmark::Event::Html(_) => events.push((range, MarkdownEvent::Html)),
|
||||
pulldown_cmark::Event::InlineHtml(_) => events.push((range, MarkdownEvent::InlineHtml)),
|
||||
pulldown_cmark::Event::FootnoteReference(_) => {
|
||||
events.push((range, MarkdownEvent::FootnoteReference))
|
||||
}
|
||||
pulldown_cmark::Event::SoftBreak => events.push((range, MarkdownEvent::SoftBreak)),
|
||||
pulldown_cmark::Event::HardBreak => events.push((range, MarkdownEvent::HardBreak)),
|
||||
pulldown_cmark::Event::Rule => events.push((range, MarkdownEvent::Rule)),
|
||||
pulldown_cmark::Event::TaskListMarker(checked) => {
|
||||
events.push((range, MarkdownEvent::TaskListMarker(checked)))
|
||||
}
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
/// A static-lifetime equivalent of pulldown_cmark::Event so we can cache the
|
||||
/// parse result for rendering without resorting to unsafe lifetime coercion.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum MarkdownEvent {
|
||||
/// Start of a tagged element. Events that are yielded after this event
|
||||
/// and before its corresponding `End` event are inside this element.
|
||||
/// Start and end events are guaranteed to be balanced.
|
||||
Start(MarkdownTag),
|
||||
/// End of a tagged element.
|
||||
End(MarkdownTagEnd),
|
||||
/// A text node.
|
||||
Text,
|
||||
/// An inline code node.
|
||||
Code,
|
||||
/// An HTML node.
|
||||
Html,
|
||||
/// An inline HTML node.
|
||||
InlineHtml,
|
||||
/// A reference to a footnote with given label, which may or may not be defined
|
||||
/// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may
|
||||
/// occur in any order.
|
||||
FootnoteReference,
|
||||
/// A soft line break.
|
||||
SoftBreak,
|
||||
/// A hard line break.
|
||||
HardBreak,
|
||||
/// A horizontal ruler.
|
||||
Rule,
|
||||
/// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked.
|
||||
TaskListMarker(bool),
|
||||
}
|
||||
|
||||
/// Tags for elements that can contain other elements.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum MarkdownTag {
|
||||
/// A paragraph of text and other inline elements.
|
||||
Paragraph,
|
||||
|
||||
/// A heading, with optional identifier, classes and custom attributes.
|
||||
/// The identifier is prefixed with `#` and the last one in the attributes
|
||||
/// list is chosen, classes are prefixed with `.` and custom attributes
|
||||
/// have no prefix and can optionally have a value (`myattr` o `myattr=myvalue`).
|
||||
Heading {
|
||||
level: HeadingLevel,
|
||||
id: Option<SharedString>,
|
||||
classes: Vec<SharedString>,
|
||||
/// The first item of the tuple is the attr and second one the value.
|
||||
attrs: Vec<(SharedString, Option<SharedString>)>,
|
||||
},
|
||||
|
||||
BlockQuote,
|
||||
|
||||
/// A code block.
|
||||
CodeBlock(CodeBlockKind),
|
||||
|
||||
/// A HTML block.
|
||||
HtmlBlock,
|
||||
|
||||
/// A list. If the list is ordered the field indicates the number of the first item.
|
||||
/// Contains only list items.
|
||||
List(Option<u64>), // TODO: add delim and tight for ast (not needed for html)
|
||||
|
||||
/// A list item.
|
||||
Item,
|
||||
|
||||
/// A footnote definition. The value contained is the footnote's label by which it can
|
||||
/// be referred to.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
FootnoteDefinition(SharedString),
|
||||
|
||||
/// A table. Contains a vector describing the text-alignment for each of its columns.
|
||||
Table(Vec<Alignment>),
|
||||
|
||||
/// A table header. Contains only `TableCell`s. Note that the table body starts immediately
|
||||
/// after the closure of the `TableHead` tag. There is no `TableBody` tag.
|
||||
TableHead,
|
||||
|
||||
/// A table row. Is used both for header rows as body rows. Contains only `TableCell`s.
|
||||
TableRow,
|
||||
TableCell,
|
||||
|
||||
// span-level tags
|
||||
Emphasis,
|
||||
Strong,
|
||||
Strikethrough,
|
||||
|
||||
/// A link.
|
||||
Link {
|
||||
link_type: LinkType,
|
||||
dest_url: SharedString,
|
||||
title: SharedString,
|
||||
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
|
||||
id: SharedString,
|
||||
},
|
||||
|
||||
/// An image. The first field is the link type, the second the destination URL and the third is a title,
|
||||
/// the fourth is the link identifier.
|
||||
Image {
|
||||
link_type: LinkType,
|
||||
dest_url: SharedString,
|
||||
title: SharedString,
|
||||
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
|
||||
id: SharedString,
|
||||
},
|
||||
|
||||
/// A metadata block.
|
||||
MetadataBlock(MetadataBlockKind),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum CodeBlockKind {
|
||||
Indented,
|
||||
/// The value contained in the tag describes the language of the code, which may be empty.
|
||||
Fenced(SharedString),
|
||||
}
|
||||
|
||||
impl From<pulldown_cmark::Tag<'_>> for MarkdownTag {
|
||||
fn from(tag: pulldown_cmark::Tag) -> Self {
|
||||
match tag {
|
||||
pulldown_cmark::Tag::Paragraph => MarkdownTag::Paragraph,
|
||||
pulldown_cmark::Tag::Heading {
|
||||
level,
|
||||
id,
|
||||
classes,
|
||||
attrs,
|
||||
} => {
|
||||
let id = id.map(|id| SharedString::from(id.into_string()));
|
||||
let classes = classes
|
||||
.into_iter()
|
||||
.map(|c| SharedString::from(c.into_string()))
|
||||
.collect();
|
||||
let attrs = attrs
|
||||
.into_iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
SharedString::from(key.into_string()),
|
||||
value.map(|v| SharedString::from(v.into_string())),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
MarkdownTag::Heading {
|
||||
level,
|
||||
id,
|
||||
classes,
|
||||
attrs,
|
||||
}
|
||||
}
|
||||
pulldown_cmark::Tag::BlockQuote => MarkdownTag::BlockQuote,
|
||||
pulldown_cmark::Tag::CodeBlock(kind) => match kind {
|
||||
pulldown_cmark::CodeBlockKind::Indented => {
|
||||
MarkdownTag::CodeBlock(CodeBlockKind::Indented)
|
||||
}
|
||||
pulldown_cmark::CodeBlockKind::Fenced(info) => MarkdownTag::CodeBlock(
|
||||
CodeBlockKind::Fenced(SharedString::from(info.into_string())),
|
||||
),
|
||||
},
|
||||
pulldown_cmark::Tag::List(start_number) => MarkdownTag::List(start_number),
|
||||
pulldown_cmark::Tag::Item => MarkdownTag::Item,
|
||||
pulldown_cmark::Tag::FootnoteDefinition(label) => {
|
||||
MarkdownTag::FootnoteDefinition(SharedString::from(label.to_string()))
|
||||
}
|
||||
pulldown_cmark::Tag::Table(alignments) => MarkdownTag::Table(alignments),
|
||||
pulldown_cmark::Tag::TableHead => MarkdownTag::TableHead,
|
||||
pulldown_cmark::Tag::TableRow => MarkdownTag::TableRow,
|
||||
pulldown_cmark::Tag::TableCell => MarkdownTag::TableCell,
|
||||
pulldown_cmark::Tag::Emphasis => MarkdownTag::Emphasis,
|
||||
pulldown_cmark::Tag::Strong => MarkdownTag::Strong,
|
||||
pulldown_cmark::Tag::Strikethrough => MarkdownTag::Strikethrough,
|
||||
pulldown_cmark::Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
} => MarkdownTag::Link {
|
||||
link_type,
|
||||
dest_url: SharedString::from(dest_url.into_string()),
|
||||
title: SharedString::from(title.into_string()),
|
||||
id: SharedString::from(id.into_string()),
|
||||
},
|
||||
pulldown_cmark::Tag::Image {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
} => MarkdownTag::Image {
|
||||
link_type,
|
||||
dest_url: SharedString::from(dest_url.into_string()),
|
||||
title: SharedString::from(title.into_string()),
|
||||
id: SharedString::from(id.into_string()),
|
||||
},
|
||||
pulldown_cmark::Tag::HtmlBlock => MarkdownTag::HtmlBlock,
|
||||
pulldown_cmark::Tag::MetadataBlock(kind) => MarkdownTag::MetadataBlock(kind),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ impl StoryContainer {
|
|||
}
|
||||
|
||||
impl ParentElement for StoryContainer {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
@ -372,7 +372,7 @@ impl RenderOnce for StorySection {
|
|||
}
|
||||
|
||||
impl ParentElement for StorySection {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -407,7 +407,7 @@ impl VisibleOnHover for ButtonLike {
|
|||
}
|
||||
|
||||
impl ParentElement for ButtonLike {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ impl LabelCommon for LabelLike {
|
|||
}
|
||||
|
||||
impl ParentElement for LabelLike {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ impl List {
|
|||
}
|
||||
|
||||
impl ParentElement for List {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ impl Selectable for ListItem {
|
|||
}
|
||||
|
||||
impl ParentElement for ListItem {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ impl ModalHeader {
|
|||
}
|
||||
|
||||
impl ParentElement for ModalHeader {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ impl ModalContent {
|
|||
}
|
||||
|
||||
impl ParentElement for ModalContent {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ impl ModalRow {
|
|||
}
|
||||
|
||||
impl ParentElement for ModalRow {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ impl Popover {
|
|||
}
|
||||
|
||||
impl ParentElement for Popover {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ impl Selectable for Tab {
|
|||
}
|
||||
|
||||
impl ParentElement for Tab {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ impl TabBar {
|
|||
}
|
||||
|
||||
impl ParentElement for TabBar {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ impl InteractiveElement for TitleBar {
|
|||
impl StatefulInteractiveElement for TitleBar {}
|
||||
|
||||
impl ParentElement for TitleBar {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -983,7 +983,7 @@ mod element {
|
|||
}
|
||||
|
||||
impl ParentElement for PaneAxisElement {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue