markdown: Improve table cell alignment (#53465)

David Alecrim and Smit Barmase created

## Summary

Markdown preview tables kept text pinned to the top of a row when a
neighboring cell contained a taller image. This made mixed
text-and-image tables look unbalanced and inconsistent with common
editor behavior. This change makes table text stay visually centered
within taller rows so Markdown tables are easier to scan and match
expected rendering more closely.

## Before / After

| Before | After |
| --- | --- |
| <img width="849" height="869" alt="Screenshot 2026-04-08 at 19 55 50"
src="https://github.com/user-attachments/assets/b3751bff-3750-4ca1-8997-6f5265e4d291"
/> | <img width="898" height="734" alt="Screenshot 2026-04-08 at 21 47
31"
src="https://github.com/user-attachments/assets/d853c0a1-800c-4a2a-aec9-e0ef08453fa7"
/> |

## References
Inspired by comparing this with VS Code preview
<img width="1286" height="878" alt="Screenshot 2026-04-08 at 21 54 06"
src="https://github.com/user-attachments/assets/8dbbfe28-9980-4012-94f1-1b5b3503065b"
/>

Release Notes:

- Improved Markdown preview table cells to vertically center content in
tall rows and respect column alignment from the table header.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

crates/markdown/src/html/html_rendering.rs | 31 ++++++++++
crates/markdown/src/markdown.rs            | 68 +++++++++++++++++++----
2 files changed, 85 insertions(+), 14 deletions(-)

Detailed changes

crates/markdown/src/html/html_rendering.rs 🔗

@@ -1,6 +1,8 @@
 use std::ops::Range;
 
-use gpui::{App, FontStyle, FontWeight, StrikethroughStyle, TextStyleRefinement, UnderlineStyle};
+use gpui::{
+    App, FontStyle, FontWeight, StrikethroughStyle, TextAlign, TextStyleRefinement, UnderlineStyle,
+};
 use pulldown_cmark::Alignment;
 use ui::prelude::*;
 
@@ -245,14 +247,24 @@ impl MarkdownElement {
                 }
 
                 let max_span = max_column_count.saturating_sub(column_index);
+                let text_align = match cell.alignment {
+                    Alignment::Left => TextAlign::Left,
+                    Alignment::Center => TextAlign::Center,
+                    Alignment::Right => TextAlign::Right,
+                    _ => self.style.base_text_style.text_align,
+                };
+
                 let mut cell_div = div()
                     .col_span(cell.col_span.min(max_span) as u16)
                     .row_span(cell.row_span.min(total_rows - row_index) as u16)
+                    .flex()
+                    .flex_col()
                     .when(column_index > 0, |this| this.border_l_1())
                     .when(row_index > 0, |this| this.border_t_1())
                     .border_color(cx.theme().colors().border)
                     .px_2()
                     .py_1()
+                    .h_full()
                     .when(cell.is_header, |this| {
                         this.bg(cx.theme().colors().title_bar_background)
                     })
@@ -266,7 +278,22 @@ impl MarkdownElement {
                     _ => cell_div,
                 };
 
+                builder.push_text_style(TextStyleRefinement {
+                    text_align: Some(text_align),
+                    ..Default::default()
+                });
                 builder.push_div(cell_div, &table.source_range, markdown_end);
+                builder.push_div(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .flex_1()
+                        .w_full()
+                        .justify_center()
+                        .text_align(text_align),
+                    &table.source_range,
+                    markdown_end,
+                );
                 self.render_html_paragraph(
                     &cell.children,
                     source_allocator,
@@ -275,6 +302,8 @@ impl MarkdownElement {
                     markdown_end,
                 );
                 builder.pop_div();
+                builder.pop_div();
+                builder.pop_text_style();
 
                 for row_offset in 0..cell.row_span {
                     for column_offset in 0..cell.col_span {

crates/markdown/src/markdown.rs 🔗

@@ -2000,20 +2000,49 @@ impl Element for MarkdownElement {
                             let is_header = builder.table.in_head;
                             let row_index = builder.table.row_index;
                             let col_index = builder.table.col_index;
+                            let alignment = builder.table.alignments.get(col_index).copied();
+                            let text_align = match alignment {
+                                Some(Alignment::Left) => TextAlign::Left,
+                                Some(Alignment::Center) => TextAlign::Center,
+                                Some(Alignment::Right) => TextAlign::Right,
+                                _ => self.style.base_text_style.text_align,
+                            };
+
+                            let mut cell_div = div()
+                                .flex()
+                                .flex_col()
+                                .h_full()
+                                .when(col_index > 0, |this| this.border_l_1())
+                                .when(row_index > 0, |this| this.border_t_1())
+                                .border_color(cx.theme().colors().border)
+                                .px_1()
+                                .py_0p5()
+                                .when(is_header, |this| {
+                                    this.bg(cx.theme().colors().title_bar_background)
+                                })
+                                .when(!is_header && row_index % 2 == 1, |this| {
+                                    this.bg(cx.theme().colors().panel_background)
+                                });
+
+                            cell_div = match alignment {
+                                Some(Alignment::Center) => cell_div.items_center(),
+                                Some(Alignment::Right) => cell_div.items_end(),
+                                _ => cell_div,
+                            };
 
+                            builder.push_text_style(TextStyleRefinement {
+                                text_align: Some(text_align),
+                                ..Default::default()
+                            });
+                            builder.push_div(cell_div, range, markdown_end);
                             builder.push_div(
                                 div()
-                                    .when(col_index > 0, |this| this.border_l_1())
-                                    .when(row_index > 0, |this| this.border_t_1())
-                                    .border_color(cx.theme().colors().border)
-                                    .px_1()
-                                    .py_0p5()
-                                    .when(is_header, |this| {
-                                        this.bg(cx.theme().colors().title_bar_background)
-                                    })
-                                    .when(!is_header && row_index % 2 == 1, |this| {
-                                        this.bg(cx.theme().colors().panel_background)
-                                    }),
+                                    .flex()
+                                    .flex_col()
+                                    .flex_1()
+                                    .w_full()
+                                    .justify_center()
+                                    .text_align(text_align),
                                 range,
                                 markdown_end,
                             );
@@ -2113,6 +2142,8 @@ impl Element for MarkdownElement {
                     MarkdownTagEnd::TableCell => {
                         builder.replace_pending_checkbox(self.on_checkbox_toggle.clone());
                         builder.pop_div();
+                        builder.pop_div();
+                        builder.pop_text_style();
                         builder.table.end_cell();
                     }
                     MarkdownTagEnd::FootnoteDefinition => {
@@ -2702,7 +2733,7 @@ impl MarkdownElementBuilder {
         )
         .fill();
 
-        let element = if let Some(on_toggle) = on_toggle {
+        let checkbox = if let Some(on_toggle) = on_toggle {
             checkbox
                 .on_click(move |_state, window, cx| {
                     on_toggle(marker_source.clone(), !checked, window, cx);
@@ -2711,7 +2742,18 @@ impl MarkdownElementBuilder {
         } else {
             checkbox.visualization_only(true).into_any_element()
         };
-        self.div_stack.last_mut().unwrap().extend([element]);
+
+        let mut checkbox_container = h_flex().w_full();
+        checkbox_container = match self.text_style().text_align {
+            TextAlign::Left => checkbox_container.justify_start(),
+            TextAlign::Center => checkbox_container.justify_center(),
+            TextAlign::Right => checkbox_container.justify_end(),
+        };
+
+        self.div_stack
+            .last_mut()
+            .unwrap()
+            .extend([checkbox_container.child(checkbox).into_any_element()]);
     }
 
     fn source_range_for_rendered(&self, rendered: &Range<usize>) -> Option<Range<usize>> {