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:
Antonio Scandurra
2024-05-09 11:03:33 +02:00
committed by GitHub
co-authored by Nathan Sobo Conrad Alp Zachiah Sawyer
parent ddaaaee973
commit 5df1481297
24 changed files with 1629 additions and 88 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
}
}
+2 -2
View File
@@ -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)
}
}
+132 -55
View File
@@ -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)
},
+57 -13
View File
@@ -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 {