mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
markdown: Render checkboxes in markdown table cells (#50595)
Render `[x]` and `[ ]` as checkbox widgets when they appear as the sole content of a markdown table cell. Previously these were displayed as raw text. List-item checkboxes were already rendered correctly; this extends the same treatment to table cells. Fixes #50045. ## What this does - Table cells containing only `[x]`, `[X]`, or `[ ]` now render as visual checkboxes instead of plain text - Both markdown rendering paths are covered: the `markdown` crate (agent panel, chat) and the `markdown_preview` crate (file preview) - Checkboxes are display-only, matching the existing list-item checkbox behavior ## How it works pulldown-cmark splits `[x]` in table cells into three separate `Text` events (`[`, `x`, `]`) rather than emitting a `TaskListMarker` event (which only fires for list items per the GFM spec). The fix operates at each crate's natural interception point: - **`markdown` crate**: After all text events for a table cell have been buffered, `replace_pending_checkbox()` checks the accumulated text before the cell div is finalized. If it matches the checkbox pattern, the pending text is replaced with a `Checkbox` widget. - **`markdown_preview` crate**: In `render_markdown_text()`, text chunks whose trimmed content matches the checkbox pattern are rendered as `MarkdownCheckbox` widgets instead of `InteractiveText`. ## Scope Three files, purely additive: - `crates/markdown/src/markdown.rs` — `replace_pending_checkbox()` on builder, called at `TableCell` end - `crates/markdown_preview/src/markdown_renderer.rs` — checkbox detection in `render_markdown_text()` - `crates/markdown_preview/src/markdown_parser.rs` — test only No changes to parser data models, GPUI, or any shared infrastructure. ## What's not in scope - **HTML `<input type="checkbox">`** — pulldown-cmark strips these as raw HTML. Supporting them requires HTML tag parsing, which is a separate concern. - **Interactive (click-to-toggle) checkboxes in tables** — table checkboxes are display-only. List-item checkboxes in Zed support Cmd+click toggling, but extending that to table cells would require tracking source ranges across the split parser events, which is a separate enhancement. ## Follow-up Table checkbox interactivity (Cmd+click toggle) is straightforward to add as a follow-up — the source ranges are already available in `markdown_preview`, and the `markdown` crate would need minor callback plumbing. ## Screenshots **Markdown checkbox before** <img width="1603" height="863" alt="md-checkbox-before-1" src="https://github.com/user-attachments/assets/8539d79d-c74f-4d14-a3e5-525e4d0083aa" /> <img width="1599" height="892" alt="md-checkbox-before-2" src="https://github.com/user-attachments/assets/7badfab1-651f-4fab-8879-deb109c56670" /> **Markdown checkbox after** <img width="1832" height="889" alt="md-checkbox-after-1" src="https://github.com/user-attachments/assets/463b6334-9f50-41c0-ab7e-24d238244873" /> <img width="1795" height="886" alt="md-checkbox-after-2" src="https://github.com/user-attachments/assets/57d3d9de-1d23-42ba-bc0a-5aa0c699b13d" /> ## Test plan **Unit tests** (2 new): - `test_table_with_checkboxes` (markdown_preview) — parser delivers `[x]`/`[ ]` text into table cell structures - `test_table_checkbox_detection` (markdown) — parser events accumulate checkbox text in table cells, confirming `replace_pending_checkbox` detection logic **Automated**: - [x] `cargo test -p markdown` — 27 tests pass (26 existing + 1 new) - [x] `cargo test -p markdown_preview` — 61 tests pass (60 existing + 1 new) **Manual** (verified against `test-checkbox-table.md`): - [x] Basic `[x]`/`[ ]` in a status column - [x] Checkbox-only column alongside text - [x] Multiple checkbox columns in one table - [x] Left, center, and right column alignments - [x] Uppercase `[X]` variant - [x] Leading/trailing whitespace in cell - [x] Checkboxes alongside other inline elements (links, bold text) - [x] Single-column and minimal two-column tables - [x] Normal table text unaffected by detection - [x] List checkboxes still render correctly (regression) - [x] Agent panel: asked agent to output table with checkbox columns Release Notes: - Fixed `[x]` and `[ ]` checkboxes not rendering in markdown table cells (#50045)
This commit is contained in:
parent
945f642478
commit
bb769b937a
3 changed files with 112 additions and 0 deletions
|
|
@ -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<usize>) {
|
||||
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<String> = 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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue