diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 087b7153704c215ec27eae653879ffe9f11ebf09..7605c53c788363b4eec42a2151a258a137ce84a6 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1441,6 +1441,7 @@ impl Element for MarkdownElement { builder.table.end_row(); } MarkdownTagEnd::TableCell => { + builder.replace_pending_checkbox(range); builder.pop_div(); builder.table.end_cell(); } @@ -1926,6 +1927,28 @@ impl MarkdownElementBuilder { } } + fn replace_pending_checkbox(&mut self, source_range: &Range) { + let trimmed = self.pending_line.text.trim(); + if trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]" { + let checked = trimmed != "[ ]"; + self.pending_line = PendingLine::default(); + let checkbox = Checkbox::new( + ElementId::Name( + format!("table_checkbox_{}_{}", source_range.start, source_range.end).into(), + ), + if checked { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + ) + .fill() + .visualization_only(true) + .into_any_element(); + self.div_stack.last_mut().unwrap().extend([checkbox]); + } + } + fn flush_text(&mut self) { let line = mem::take(&mut self.pending_line); if line.text.is_empty() { @@ -2493,6 +2516,48 @@ mod tests { assert_eq!(second_word, "b"); } + #[test] + fn test_table_checkbox_detection() { + let md = "| Done |\n|------|\n| [x] |\n| [ ] |"; + let (events, _, _) = crate::parser::parse_markdown(md); + + let mut in_table = false; + let mut cell_texts: Vec = Vec::new(); + let mut current_cell = String::new(); + + for (range, event) in &events { + match event { + MarkdownEvent::Start(MarkdownTag::Table(_)) => in_table = true, + MarkdownEvent::End(MarkdownTagEnd::Table) => in_table = false, + MarkdownEvent::Start(MarkdownTag::TableCell) => current_cell.clear(), + MarkdownEvent::End(MarkdownTagEnd::TableCell) => { + if in_table { + cell_texts.push(current_cell.clone()); + } + } + MarkdownEvent::Text if in_table => { + current_cell.push_str(&md[range.clone()]); + } + _ => {} + } + } + + let checkbox_cells: Vec<&String> = cell_texts + .iter() + .filter(|t| { + let trimmed = t.trim(); + trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]" + }) + .collect(); + assert_eq!( + checkbox_cells.len(), + 2, + "Expected 2 checkbox cells, got: {cell_texts:?}" + ); + assert_eq!(checkbox_cells[0].trim(), "[x]"); + assert_eq!(checkbox_cells[1].trim(), "[ ]"); + } + #[gpui::test] fn test_inline_code_word_selection_excludes_backticks(cx: &mut TestAppContext) { // Test that double-clicking on inline code selects just the code content, diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index 29ea273f49578bd6ad408a8d57b891f572705c07..40a1ed804f750a7e3173a76643ad1f6b1a362bd3 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -2776,6 +2776,35 @@ Some other content ); } + #[gpui::test] + async fn test_table_with_checkboxes() { + let markdown = "\ +| Done | Task | +|------|---------| +| [x] | Fix bug | +| [ ] | Add feature |"; + + let parsed = parse(markdown).await; + let table = match &parsed.children[0] { + ParsedMarkdownElement::Table(table) => table, + other => panic!("Expected table, got: {:?}", other), + }; + + let first_cell = &table.body[0].columns[0]; + let first_cell_text = match &first_cell.children[0] { + MarkdownParagraphChunk::Text(t) => t.contents.to_string(), + other => panic!("Expected text chunk, got: {:?}", other), + }; + assert_eq!(first_cell_text.trim(), "[x]"); + + let second_cell = &table.body[1].columns[0]; + let second_cell_text = match &second_cell.children[0] { + MarkdownParagraphChunk::Text(t) => t.contents.to_string(), + other => panic!("Expected text chunk, got: {:?}", other), + }; + assert_eq!(second_cell_text.trim(), "[ ]"); + } + #[gpui::test] async fn test_list_basic() { let parsed = parse( diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index a6666fd97dccf2adef7e70ac374314cd55ff5821..96adc670d91f51b343c388403fa5aa7ebad678ee 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -891,6 +891,24 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) for parsed_region in parsed_new { match parsed_region { MarkdownParagraphChunk::Text(parsed) => { + let trimmed = parsed.contents.trim(); + if trimmed == "[x]" || trimmed == "[X]" || trimmed == "[ ]" { + let checked = trimmed != "[ ]"; + let element = div() + .child(MarkdownCheckbox::new( + cx.next_id(&parsed.source_range), + if checked { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + cx.clone(), + )) + .into_any(); + any_element.push(element); + continue; + } + let element_id = cx.next_id(&parsed.source_range); let highlights = gpui::combine_highlights(