Detailed changes
@@ -104,25 +104,34 @@ pub enum HeadingLevel {
#[derive(Debug)]
pub struct ParsedMarkdownTable {
pub source_range: Range<usize>,
- pub header: ParsedMarkdownTableRow,
+ pub header: Vec<ParsedMarkdownTableRow>,
pub body: Vec<ParsedMarkdownTableRow>,
pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
}
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(test, derive(PartialEq))]
pub enum ParsedMarkdownTableAlignment {
- /// Default text alignment.
+ #[default]
None,
Left,
Center,
Right,
}
+#[derive(Debug)]
+#[cfg_attr(test, derive(PartialEq))]
+pub struct ParsedMarkdownTableColumn {
+ pub col_span: usize,
+ pub row_span: usize,
+ pub is_header: bool,
+ pub children: MarkdownParagraph,
+}
+
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ParsedMarkdownTableRow {
- pub children: Vec<MarkdownParagraph>,
+ pub columns: Vec<ParsedMarkdownTableColumn>,
}
impl Default for ParsedMarkdownTableRow {
@@ -134,12 +143,12 @@ impl Default for ParsedMarkdownTableRow {
impl ParsedMarkdownTableRow {
pub fn new() -> Self {
Self {
- children: Vec::new(),
+ columns: Vec::new(),
}
}
- pub fn with_children(children: Vec<MarkdownParagraph>) -> Self {
- Self { children }
+ pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
+ Self { columns }
}
}
@@ -462,9 +462,9 @@ impl<'a> MarkdownParser<'a> {
fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
let (_event, source_range) = self.previous().unwrap();
let source_range = source_range.clone();
- let mut header = ParsedMarkdownTableRow::new();
+ let mut header = vec![];
let mut body = vec![];
- let mut current_row = vec![];
+ let mut row_columns = vec![];
let mut in_header = true;
let column_alignments = alignment.iter().map(Self::convert_alignment).collect();
@@ -484,17 +484,21 @@ impl<'a> MarkdownParser<'a> {
Event::Start(Tag::TableCell) => {
self.cursor += 1;
let cell_contents = self.parse_text(false, Some(source_range));
- current_row.push(cell_contents);
+ row_columns.push(ParsedMarkdownTableColumn {
+ col_span: 1,
+ row_span: 1,
+ is_header: in_header,
+ children: cell_contents,
+ });
}
Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
self.cursor += 1;
- let new_row = std::mem::take(&mut current_row);
+ let columns = std::mem::take(&mut row_columns);
if in_header {
- header.children = new_row;
+ header.push(ParsedMarkdownTableRow { columns: columns });
in_header = false;
} else {
- let row = ParsedMarkdownTableRow::with_children(new_row);
- body.push(row);
+ body.push(ParsedMarkdownTableRow::with_columns(columns));
}
}
Event::End(TagEnd::Table) => {
@@ -941,6 +945,70 @@ impl<'a> MarkdownParser<'a> {
}
}
+ fn parse_table_row(
+ &self,
+ source_range: Range<usize>,
+ node: &Rc<markup5ever_rcdom::Node>,
+ ) -> Option<ParsedMarkdownTableRow> {
+ let mut columns = Vec::new();
+
+ match &node.data {
+ markup5ever_rcdom::NodeData::Element { name, .. } => {
+ if local_name!("tr") != name.local {
+ return None;
+ }
+
+ for node in node.children.borrow().iter() {
+ if let Some(column) = self.parse_table_column(source_range.clone(), node) {
+ columns.push(column);
+ }
+ }
+ }
+ _ => {}
+ }
+
+ if columns.is_empty() {
+ None
+ } else {
+ Some(ParsedMarkdownTableRow { columns })
+ }
+ }
+
+ fn parse_table_column(
+ &self,
+ source_range: Range<usize>,
+ node: &Rc<markup5ever_rcdom::Node>,
+ ) -> Option<ParsedMarkdownTableColumn> {
+ match &node.data {
+ markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
+ if !matches!(name.local, local_name!("th") | local_name!("td")) {
+ return None;
+ }
+
+ let mut children = MarkdownParagraph::new();
+ self.consume_paragraph(source_range, node, &mut children);
+
+ Some(ParsedMarkdownTableColumn {
+ col_span: std::cmp::max(
+ Self::attr_value(attrs, local_name!("colspan"))
+ .and_then(|span| span.parse().ok())
+ .unwrap_or(1),
+ 1,
+ ),
+ row_span: std::cmp::max(
+ Self::attr_value(attrs, local_name!("rowspan"))
+ .and_then(|span| span.parse().ok())
+ .unwrap_or(1),
+ 1,
+ ),
+ is_header: matches!(name.local, local_name!("th")),
+ children,
+ })
+ }
+ _ => None,
+ }
+ }
+
fn consume_children(
&self,
source_range: Range<usize>,
@@ -1056,7 +1124,7 @@ impl<'a> MarkdownParser<'a> {
node: &Rc<markup5ever_rcdom::Node>,
source_range: Range<usize>,
) -> Option<ParsedMarkdownTable> {
- let mut header_columns = Vec::new();
+ let mut header_rows = Vec::new();
let mut body_rows = Vec::new();
// node should be a thead or tbody element
@@ -1066,21 +1134,16 @@ impl<'a> MarkdownParser<'a> {
if local_name!("thead") == name.local {
// node should be a tr element
for node in node.children.borrow().iter() {
- let mut paragraph = MarkdownParagraph::new();
- self.consume_paragraph(source_range.clone(), node, &mut paragraph);
-
- for paragraph in paragraph.into_iter() {
- header_columns.push(vec![paragraph]);
+ if let Some(row) = self.parse_table_row(source_range.clone(), node) {
+ header_rows.push(row);
}
}
} else if local_name!("tbody") == name.local {
// node should be a tr element
for node in node.children.borrow().iter() {
- let mut row = MarkdownParagraph::new();
- self.consume_paragraph(source_range.clone(), node, &mut row);
- body_rows.push(ParsedMarkdownTableRow::with_children(
- row.into_iter().map(|column| vec![column]).collect(),
- ));
+ if let Some(row) = self.parse_table_row(source_range.clone(), node) {
+ body_rows.push(row);
+ }
}
}
}
@@ -1088,12 +1151,12 @@ impl<'a> MarkdownParser<'a> {
}
}
- if !header_columns.is_empty() || !body_rows.is_empty() {
+ if !header_rows.is_empty() || !body_rows.is_empty() {
Some(ParsedMarkdownTable {
source_range,
body: body_rows,
column_alignments: Vec::default(),
- header: ParsedMarkdownTableRow::with_children(header_columns),
+ header: header_rows,
})
} else {
None
@@ -1589,10 +1652,19 @@ mod tests {
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..366,
- row(vec![text("Id", 0..366), text("Name ", 0..366)]),
+ vec![row(vec![
+ column(1, 1, true, text("Id", 0..366)),
+ column(1, 1, true, text("Name ", 0..366))
+ ])],
vec![
- row(vec![text("1", 0..366), text("Chris", 0..366)]),
- row(vec![text("2", 0..366), text("Dennis", 0..366)]),
+ row(vec![
+ column(1, 1, false, text("1", 0..366)),
+ column(1, 1, false, text("Chris", 0..366))
+ ]),
+ row(vec![
+ column(1, 1, false, text("2", 0..366)),
+ column(1, 1, false, text("Dennis", 0..366))
+ ]),
],
))],
},
@@ -1622,10 +1694,16 @@ mod tests {
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..240,
- row(vec![]),
+ vec![],
vec![
- row(vec![text("1", 0..240), text("Chris", 0..240)]),
- row(vec![text("2", 0..240), text("Dennis", 0..240)]),
+ row(vec![
+ column(1, 1, false, text("1", 0..240)),
+ column(1, 1, false, text("Chris", 0..240))
+ ]),
+ row(vec![
+ column(1, 1, false, text("2", 0..240)),
+ column(1, 1, false, text("Dennis", 0..240))
+ ]),
],
))],
},
@@ -1651,7 +1729,10 @@ mod tests {
ParsedMarkdown {
children: vec![ParsedMarkdownElement::Table(table(
0..150,
- row(vec![text("Id", 0..150), text("Name", 0..150)]),
+ vec![row(vec![
+ column(1, 1, true, text("Id", 0..150)),
+ column(1, 1, true, text("Name", 0..150))
+ ])],
vec![],
))],
},
@@ -1833,7 +1914,10 @@ Some other content
let expected_table = table(
0..48,
- row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
+ vec![row(vec![
+ column(1, 1, true, text("Header 1", 1..11)),
+ column(1, 1, true, text("Header 2", 12..22)),
+ ])],
vec![],
);
@@ -1853,10 +1937,19 @@ Some other content
let expected_table = table(
0..95,
- row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
+ vec![row(vec![
+ column(1, 1, true, text("Header 1", 1..11)),
+ column(1, 1, true, text("Header 2", 12..22)),
+ ])],
vec![
- row(vec![text("Cell 1", 49..59), text("Cell 2", 60..70)]),
- row(vec![text("Cell 3", 73..83), text("Cell 4", 84..94)]),
+ row(vec![
+ column(1, 1, false, text("Cell 1", 49..59)),
+ column(1, 1, false, text("Cell 2", 60..70)),
+ ]),
+ row(vec![
+ column(1, 1, false, text("Cell 3", 73..83)),
+ column(1, 1, false, text("Cell 4", 84..94)),
+ ]),
],
);
@@ -2313,7 +2406,7 @@ fn main() {
fn table(
source_range: Range<usize>,
- header: ParsedMarkdownTableRow,
+ header: Vec<ParsedMarkdownTableRow>,
body: Vec<ParsedMarkdownTableRow>,
) -> ParsedMarkdownTable {
ParsedMarkdownTable {
@@ -2324,8 +2417,22 @@ fn main() {
}
}
- fn row(children: Vec<MarkdownParagraph>) -> ParsedMarkdownTableRow {
- ParsedMarkdownTableRow { children }
+ fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
+ ParsedMarkdownTableRow { columns }
+ }
+
+ fn column(
+ col_span: usize,
+ row_span: usize,
+ is_header: bool,
+ children: MarkdownParagraph,
+ ) -> ParsedMarkdownTableColumn {
+ ParsedMarkdownTableColumn {
+ col_span,
+ row_span,
+ is_header,
+ children,
+ }
}
impl PartialEq for ParsedMarkdownTable {
@@ -8,8 +8,8 @@ use fs::normalize_path;
use gpui::{
AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div,
Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement,
- Keystroke, Length, Modifiers, ParentElement, Render, Resource, SharedString, Styled,
- StyledText, TextStyle, WeakEntity, Window, div, img, rems,
+ Keystroke, Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText,
+ TextStyle, WeakEntity, Window, div, img, rems,
};
use settings::Settings;
use std::{
@@ -19,8 +19,10 @@ use std::{
};
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
use ui::{
- Clickable, FluentBuilder, LinkPreview, StatefulInteractiveElement, StyledExt, StyledImage,
- ToggleState, Tooltip, VisibleOnHover, prelude::*, tooltip_container,
+ ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize,
+ InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems,
+ StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover,
+ h_flex, tooltip_container, v_flex,
};
use workspace::{OpenOptions, OpenVisible, Workspace};
@@ -467,132 +469,100 @@ impl gpui::RenderOnce for MarkdownCheckbox {
}
}
-fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize {
- paragraphs
- .iter()
- .map(|paragraph| match paragraph {
- MarkdownParagraphChunk::Text(text) => text.contents.len(),
- // TODO: Scale column width based on image size
- MarkdownParagraphChunk::Image(_) => 1,
- })
- .sum()
+fn calculate_table_columns_count(rows: &Vec<ParsedMarkdownTableRow>) -> usize {
+ let mut actual_column_count = 0;
+ for row in rows {
+ actual_column_count = actual_column_count.max(
+ row.columns
+ .iter()
+ .map(|column| column.col_span)
+ .sum::<usize>(),
+ );
+ }
+ actual_column_count
}
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
- let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
+ let actual_header_column_count = calculate_table_columns_count(&parsed.header);
+ let actual_body_column_count = calculate_table_columns_count(&parsed.body);
+ let max_column_count = std::cmp::max(actual_header_column_count, actual_body_column_count);
- for (index, cell) in parsed.header.children.iter().enumerate() {
- let length = paragraph_len(cell);
- max_lengths[index] = length;
- }
+ let total_rows = parsed.header.len() + parsed.body.len();
- for row in &parsed.body {
- for (index, cell) in row.children.iter().enumerate() {
- let length = paragraph_len(cell);
+ // Track which grid cells are occupied by spanning cells
+ let mut grid_occupied = vec![vec![false; max_column_count]; total_rows];
- if index >= max_lengths.len() {
- max_lengths.resize(index + 1, length);
- }
-
- if length > max_lengths[index] {
- max_lengths[index] = length;
- }
- }
- }
+ let mut cells = Vec::with_capacity(total_rows * max_column_count);
- let total_max_length: usize = max_lengths.iter().sum();
- let max_column_widths: Vec<f32> = max_lengths
- .iter()
- .map(|&length| length as f32 / total_max_length as f32)
- .collect();
+ for (row_idx, row) in parsed.header.iter().chain(parsed.body.iter()).enumerate() {
+ let mut col_idx = 0;
- let header = render_markdown_table_row(
- &parsed.header,
- &parsed.column_alignments,
- &max_column_widths,
- true,
- 0,
- cx,
- );
-
- let body: Vec<AnyElement> = parsed
- .body
- .iter()
- .enumerate()
- .map(|(index, row)| {
- render_markdown_table_row(
- row,
- &parsed.column_alignments,
- &max_column_widths,
- false,
- index,
- cx,
- )
- })
- .collect();
+ for (cell_idx, cell) in row.columns.iter().enumerate() {
+ // Skip columns occupied by row-spanning cells from previous rows
+ while col_idx < max_column_count && grid_occupied[row_idx][col_idx] {
+ col_idx += 1;
+ }
- div().child(header).children(body).into_any()
-}
+ if col_idx >= max_column_count {
+ break;
+ }
-fn render_markdown_table_row(
- parsed: &ParsedMarkdownTableRow,
- alignments: &Vec<ParsedMarkdownTableAlignment>,
- max_column_widths: &Vec<f32>,
- is_header: bool,
- row_index: usize,
- cx: &mut RenderContext,
-) -> AnyElement {
- let mut items = Vec::with_capacity(parsed.children.len());
- let count = parsed.children.len();
+ let alignment = parsed
+ .column_alignments
+ .get(cell_idx)
+ .copied()
+ .unwrap_or_else(|| {
+ if cell.is_header {
+ ParsedMarkdownTableAlignment::Center
+ } else {
+ ParsedMarkdownTableAlignment::None
+ }
+ });
- for (index, cell) in parsed.children.iter().enumerate() {
- let alignment = alignments
- .get(index)
- .copied()
- .unwrap_or(ParsedMarkdownTableAlignment::None);
+ let container = match alignment {
+ ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
+ ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
+ ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
+ };
- let contents = render_markdown_text(cell, cx);
+ let cell_element = container
+ .col_span(cell.col_span.min(max_column_count - col_idx) as u16)
+ .row_span(cell.row_span.min(total_rows - row_idx) as u16)
+ .children(render_markdown_text(&cell.children, cx))
+ .px_2()
+ .py_1()
+ .border_1()
+ .size_full()
+ .border_color(cx.border_color)
+ .when(cell.is_header, |this| {
+ this.bg(cx.title_bar_background_color)
+ })
+ .when(cell.row_span > 1, |this| this.justify_center())
+ .when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color));
- let container = match alignment {
- ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(),
- ParsedMarkdownTableAlignment::Center => v_flex().items_center(),
- ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
- };
+ cells.push(cell_element);
- let max_width = max_column_widths.get(index).unwrap_or(&0.0);
- let mut cell = container
- .w(Length::Definite(relative(*max_width)))
- .h_full()
- .children(contents)
- .px_2()
- .py_1()
- .border_color(cx.border_color)
- .border_l_1();
-
- if count == index + 1 {
- cell = cell.border_r_1();
- }
+ // Mark grid positions as occupied for row-spanning cells
+ for r in 0..cell.row_span {
+ for c in 0..cell.col_span {
+ if row_idx + r < total_rows && col_idx + c < max_column_count {
+ grid_occupied[row_idx + r][col_idx + c] = true;
+ }
+ }
+ }
- if is_header {
- cell = cell.bg(cx.title_bar_background_color).opacity(0.6)
+ col_idx += cell.col_span;
}
-
- items.push(cell);
- }
-
- let mut row = h_flex().border_color(cx.border_color);
-
- if is_header {
- row = row.border_y_1();
- } else {
- row = row.border_b_1();
- }
-
- if row_index % 2 == 1 {
- row = row.bg(cx.panel_background_color)
}
- row.children(items).into_any_element()
+ cx.with_common_p(div())
+ .grid()
+ .size_full()
+ .grid_cols(max_column_count as u16)
+ .border_1()
+ .border_color(cx.border_color)
+ .children(cells)
+ .into_any()
}
fn render_markdown_block_quote(
@@ -903,3 +873,143 @@ impl Render for InteractiveMarkdownElementTooltip {
})
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::markdown_elements::ParsedMarkdownTableColumn;
+ use crate::markdown_elements::ParsedMarkdownText;
+
+ fn text(text: &str) -> MarkdownParagraphChunk {
+ MarkdownParagraphChunk::Text(ParsedMarkdownText {
+ source_range: 0..text.len(),
+ contents: SharedString::new(text),
+ highlights: Default::default(),
+ region_ranges: Default::default(),
+ regions: Default::default(),
+ })
+ }
+
+ fn column(
+ col_span: usize,
+ row_span: usize,
+ children: Vec<MarkdownParagraphChunk>,
+ ) -> ParsedMarkdownTableColumn {
+ ParsedMarkdownTableColumn {
+ col_span,
+ row_span,
+ is_header: false,
+ children,
+ }
+ }
+
+ fn column_with_row_span(
+ col_span: usize,
+ row_span: usize,
+ children: Vec<MarkdownParagraphChunk>,
+ ) -> ParsedMarkdownTableColumn {
+ ParsedMarkdownTableColumn {
+ col_span,
+ row_span,
+ is_header: false,
+ children,
+ }
+ }
+
+ #[test]
+ fn test_calculate_table_columns_count() {
+ assert_eq!(0, calculate_table_columns_count(&vec![]));
+
+ assert_eq!(
+ 1,
+ calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
+ column(1, 1, vec![text("column1")])
+ ])])
+ );
+
+ assert_eq!(
+ 2,
+ calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
+ column(1, 1, vec![text("column1")]),
+ column(1, 1, vec![text("column2")]),
+ ])])
+ );
+
+ assert_eq!(
+ 2,
+ calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
+ column(2, 1, vec![text("column1")])
+ ])])
+ );
+
+ assert_eq!(
+ 3,
+ calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![
+ column(1, 1, vec![text("column1")]),
+ column(2, 1, vec![text("column2")]),
+ ])])
+ );
+
+ assert_eq!(
+ 2,
+ calculate_table_columns_count(&vec![
+ ParsedMarkdownTableRow::with_columns(vec![
+ column(1, 1, vec![text("column1")]),
+ column(1, 1, vec![text("column2")]),
+ ]),
+ ParsedMarkdownTableRow::with_columns(vec![column(1, 1, vec![text("column1")]),])
+ ])
+ );
+
+ assert_eq!(
+ 3,
+ calculate_table_columns_count(&vec![
+ ParsedMarkdownTableRow::with_columns(vec![
+ column(1, 1, vec![text("column1")]),
+ column(1, 1, vec![text("column2")]),
+ ]),
+ ParsedMarkdownTableRow::with_columns(vec![column(3, 3, vec![text("column1")]),])
+ ])
+ );
+ }
+
+ #[test]
+ fn test_row_span_support() {
+ assert_eq!(
+ 3,
+ calculate_table_columns_count(&vec![
+ ParsedMarkdownTableRow::with_columns(vec![
+ column_with_row_span(1, 2, vec![text("spans 2 rows")]),
+ column(1, 1, vec![text("column2")]),
+ column(1, 1, vec![text("column3")]),
+ ]),
+ ParsedMarkdownTableRow::with_columns(vec![
+ // First column is covered by row span from above
+ column(1, 1, vec![text("column2 row2")]),
+ column(1, 1, vec![text("column3 row2")]),
+ ])
+ ])
+ );
+
+ assert_eq!(
+ 4,
+ calculate_table_columns_count(&vec![
+ ParsedMarkdownTableRow::with_columns(vec![
+ column_with_row_span(1, 3, vec![text("spans 3 rows")]),
+ column_with_row_span(2, 1, vec![text("spans 2 cols")]),
+ column(1, 1, vec![text("column4")]),
+ ]),
+ ParsedMarkdownTableRow::with_columns(vec![
+ // First column covered by row span
+ column(1, 1, vec![text("column2")]),
+ column(1, 1, vec![text("column3")]),
+ column(1, 1, vec![text("column4")]),
+ ]),
+ ParsedMarkdownTableRow::with_columns(vec![
+ // First column still covered by row span
+ column(3, 1, vec![text("spans 3 cols")]),
+ ])
+ ])
+ );
+ }
+}