Introduce a new `markdown` crate (#11556)

Antonio Scandurra , Nathan Sobo , Conrad , Alp , and Zachiah Sawyer created

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>

Change summary

Cargo.lock                                                |  21 
Cargo.toml                                                |   2 
crates/collab_ui/src/notifications/collab_notification.rs |   2 
crates/extensions_ui/src/components/extension_card.rs     |   2 
crates/gpui/src/element.rs                                |   2 
crates/gpui/src/elements/anchored.rs                      |   2 
crates/gpui/src/elements/div.rs                           |   4 
crates/gpui/src/elements/text.rs                          | 187 +
crates/gpui/src/text_system/line_layout.rs                |  70 
crates/markdown/Cargo.toml                                |  40 
crates/markdown/examples/markdown.rs                      | 181 +
crates/markdown/src/markdown.rs                           | 902 +++++++++
crates/markdown/src/parser.rs                             | 274 ++
crates/story/src/story.rs                                 |   4 
crates/ui/src/components/button/button_like.rs            |   2 
crates/ui/src/components/label/label_like.rs              |   2 
crates/ui/src/components/list/list.rs                     |   2 
crates/ui/src/components/list/list_item.rs                |   2 
crates/ui/src/components/modal.rs                         |   6 
crates/ui/src/components/popover.rs                       |   2 
crates/ui/src/components/tab.rs                           |   2 
crates/ui/src/components/tab_bar.rs                       |   2 
crates/ui/src/components/title_bar/title_bar.rs           |   2 
crates/workspace/src/pane_group.rs                        |   2 
24 files changed, 1,629 insertions(+), 88 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -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"

Cargo.toml 🔗

@@ -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" }

crates/collab_ui/src/notifications/collab_notification.rs 🔗

@@ -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)
     }
 }

crates/extensions_ui/src/components/extension_card.rs 🔗

@@ -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)
     }
 }

crates/gpui/src/element.rs 🔗

@@ -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

crates/gpui/src/elements/anchored.rs 🔗

@@ -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)
     }
 }

crates/gpui/src/elements/div.rs 🔗

@@ -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)
     }
 }

crates/gpui/src/elements/text.rs 🔗

@@ -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)
             },

crates/gpui/src/text_system/line_layout.rs 🔗

@@ -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 {

crates/markdown/Cargo.toml 🔗

@@ -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"] }

crates/markdown/examples/markdown.rs 🔗

@@ -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!
+![This is an image](/images/logo.png)
+```
+
+## 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())
+    }
+}

crates/markdown/src/markdown.rs 🔗

@@ -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))
+    }
+}

crates/markdown/src/parser.rs 🔗

@@ -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),
+        }
+    }
+}

crates/story/src/story.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/button/button_like.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/label/label_like.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/list/list.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/list/list_item.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/modal.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/popover.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/tab.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/tab_bar.rs 🔗

@@ -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)
     }
 }

crates/ui/src/components/title_bar/title_bar.rs 🔗

@@ -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)
     }
 }

crates/workspace/src/pane_group.rs 🔗

@@ -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)
         }
     }