documents: pictures draw in table cells; Markdown-path comments within ten words
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 21s
CI / crate (push) Failing after 11m44s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

A cell holds blocks, split at its pictures as a paragraph is. A picture
takes its natural width, capped by its container.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 05:12:16 -04:00
1 parent bbb607106a
commit 4801555e0d
6 files changed
+84 -43

No files matched your search

+6 -6
View File
@@ -58,7 +58,7 @@ fn leading(text: &str, ch: char) -> usize {
.sum()
}
/// How many bytes at each end of `span` write `mark`: its asterisks, tildes or backticks.
/// Bytes at each end of `span` that write `mark`.
pub(crate) fn edge(span: &str, mark: Mark) -> usize {
let first = span.chars().next().expect("a styled span has its marker");
let run = leading(span, first).min(span.len() / 2);
@@ -76,7 +76,7 @@ pub(crate) fn walk<'a>(node: &'a mdast::Node, out: &mut Vec<&'a mdast::Node>) {
}
}
/// The marks over `text`'s bytes: styles, code, links, headings and quotes, and the syntax that writes each.
/// The marks over `text`, with the syntax writing each.
pub fn marks(text: &str) -> Vec<(Range<usize>, Mark)> {
let root = tree(text);
let mut nodes = Vec::new();
@@ -133,7 +133,7 @@ pub fn marks(text: &str) -> Vec<(Range<usize>, Mark)> {
out
}
/// Markdown styled as it is typed: strong and headings heavier, emphasis slanted, strikes struck, code washed, links in their tone; the syntax that writes them recedes.
/// Markdown styled as typed; its syntax recedes.
pub fn markdown_highlights(text: &str, cx: &App) -> Vec<(Range<usize>, Highlight)> {
let colors = &cx.theme().colors;
let plain = Highlight::new(colors.fg);
@@ -174,7 +174,7 @@ pub fn markdown_highlights(text: &str, cx: &App) -> Vec<(Range<usize>, Highlight
.collect()
}
/// A link written `[text](address)` around byte `at`: its range, its text and its address.
/// The `[text](address)` link around byte `at`, if any.
pub fn link_at(text: &str, at: usize) -> Option<(Range<usize>, String, String)> {
let root = tree(text);
let mut nodes = Vec::new();
@@ -208,7 +208,7 @@ pub fn block_ranges(text: &str) -> Vec<Range<usize>> {
.collect()
}
/// Words in `text` as a reader counts them; each ideograph counts as one.
/// Words in `text`; each ideograph counts as one.
pub fn words(text: &str) -> usize {
text.unicode_words().count()
}
@@ -216,7 +216,7 @@ pub fn words(text: &str) -> usize {
/// Words an adult reads in a minute, on average.
const PACE: usize = 238;
/// Minutes to read `words`, at least one when there are any.
/// Minutes to read `words`; at least one if any.
pub fn reading_minutes(words: usize) -> usize {
words.div_ceil(PACE)
}
+9 -9
View File
@@ -23,7 +23,7 @@ use crate::{
/// How a markdown editor shows its text.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MarkdownMode {
/// The document as it reads; a pressed block opens as its source.
/// The document as read; a pressed block opens as source.
Visual,
/// The markdown, styled as it is typed.
Source,
@@ -52,7 +52,7 @@ impl MarkdownMode {
type OnMode = Rc<dyn Fn(MarkdownMode, &mut Window, &mut App)>;
/// The block open as its source: the document it opened in, where, its own field, and whether it adds a block at the end.
/// The open block: its document, range, field, and appending.
struct Open {
document: String,
range: Range<usize>,
@@ -66,7 +66,7 @@ struct Visual {
open: Option<Open>,
}
/// `range` moved by an edit that wrote `written` bytes over `replaced`, when it lies after it.
/// `range` shifted by `written` bytes written over `replaced`.
fn shifted(range: Range<usize>, replaced: &Range<usize>, written: usize) -> Range<usize> {
if range.start < replaced.end {
return range;
@@ -78,7 +78,7 @@ fn shifted(range: Range<usize>, replaced: &Range<usize>, written: usize) -> Rang
start..end
}
/// What joins a block added at the end to the text before it.
/// What joins an appended block to the text before.
pub(crate) fn joint(document: &str) -> &'static str {
if document.is_empty() || document.ends_with("\n\n") {
""
@@ -89,7 +89,7 @@ pub(crate) fn joint(document: &str) -> &'static str {
}
}
/// Writes the open block back over its range as one undo step, and closes it; the range replaced and the bytes written, none when the text left the range.
/// Writes the open block back as one step; the edit.
fn commit(
state: &Entity<Visual>,
field: &Entity<TextInput>,
@@ -123,7 +123,7 @@ fn commit(
Some((open.range, written.len()))
}
/// Opens the block at `range` in a field of its own, so undo stays with it; the caret at its start or end.
/// Opens the block at `range` in its own field.
fn open(
state: &Entity<Visual>,
field: &Entity<TextInput>,
@@ -168,7 +168,7 @@ fn open(
});
}
/// Whether the caret sits on the field's first line, or with `last` its last.
/// The caret on the first line, or with `last` last.
pub(crate) fn at_edge(input: &TextInput, last: bool) -> bool {
let edge = if last { input.text().len() } else { 0 };
match (input.bounds_for(input.cursor()), input.bounds_for(edge)) {
@@ -177,7 +177,7 @@ pub(crate) fn at_edge(input: &TextInput, last: bool) -> bool {
}
}
/// Markdown written three ways: visual, where the document reads as it will and a pressed block opens as its source; source, styled as it is typed; and split, the source beside the document. The toolbar, links, suggestions and counts come with each.
/// Markdown written as visual, source, or split views.
#[derive(IntoElement)]
pub struct MarkdownEditor {
id: ElementId,
@@ -267,7 +267,7 @@ impl RenderOnce for MarkdownEditor {
}
}
/// The visual body: each block rendered, the open one as its source.
/// The visual body: blocks drawn, the open one as source.
fn visual(
editor: MarkdownEditor,
state: Entity<Visual>,
+2 -2
View File
@@ -194,8 +194,8 @@ fn picture(shown: Picture, window: &mut Window, cx: &mut App) -> AnyElement {
let width = i32::from(natural.width) as f32 / smooth;
div()
.debug_selector(|| "markdown-picture".into())
.w_full()
.max_w(px(width))
.w(px(width))
.max_w_full()
.child(img(image).size_full())
.into_any_element()
}
+14 -8
View File
@@ -24,7 +24,7 @@ use crate::{
pub(crate) type OnLink = Rc<dyn Fn(&str, &mut Window, &mut App)>;
type DrawCode = Rc<dyn Fn(ElementId, Option<SharedString>, SharedString) -> AnyElement>;
/// Markdown as a document: headings, prose with its styles and links, lists and tasks, quotes, code colored as code, tables, rules, math and footnotes. A press on a link opens it, or hands it to the host.
/// Markdown drawn as a document; links go to the host.
#[derive(IntoElement)]
pub struct MarkdownRenderer {
id: ElementId,
@@ -61,13 +61,13 @@ impl MarkdownRenderer {
self
}
/// Marks each heading for a table of contents over `sections`; `outline` gives its entries.
/// Marks headings for `sections`; `outline` lists them.
pub fn sections(mut self, sections: &Entity<Sections>) -> Self {
self.sections = Some(sections.clone());
self
}
/// Draws code blocks with the owner's element, given an id, the language and the code.
/// Code blocks drawn by the owner: id, language, code.
pub fn code(
mut self,
draw: impl Fn(ElementId, Option<SharedString>, SharedString) -> AnyElement + 'static,
@@ -83,7 +83,7 @@ impl MarkdownRenderer {
}
}
/// Renders nodes with one set of links handlers and ids under one owner.
/// Draws nodes under one owner's ids and link handler.
struct Draw<'a> {
id: &'a ElementId,
on_link: &'a Option<OnLink>,
@@ -297,7 +297,7 @@ impl Draw<'_> {
}))
.into_any_element(),
Node::Table { aligns, head, rows } => {
let cell = |inline: &Inline, column: usize, header: bool, cx: &App| {
let cell = |blocks: &[Node], column: usize, header: bool, cx: &App| {
div()
.flex_1()
.flex()
@@ -313,7 +313,13 @@ impl Draw<'_> {
cell.font_weight(FontWeight::MEDIUM)
.text_color(colors.fg_muted)
})
.child(self.inline(inline, cx))
.child(
div()
.flex()
.flex_col()
.gap_1()
.children(self.nodes(blocks, cx)),
)
};
div()
.flex()
@@ -326,7 +332,7 @@ impl Draw<'_> {
div().flex().bg(colors.hover).children(
head.iter()
.enumerate()
.map(|(column, inline)| cell(inline, column, true, cx)),
.map(|(column, blocks)| cell(blocks, column, true, cx)),
),
)
.children(rows.iter().map(|row| {
@@ -337,7 +343,7 @@ impl Draw<'_> {
.children(
row.iter()
.enumerate()
.map(|(column, inline)| cell(inline, column, false, cx)),
.map(|(column, blocks)| cell(blocks, column, false, cx)),
)
}))
.into_any_element()
+25 -9
View File
@@ -317,9 +317,19 @@ impl Render for Page {
}
}
/// A 200×100 picture drawn in a `width` column.
fn drawn_picture(width: f32, cx: &mut TestAppContext) -> gpui::Size<Pixels> {
let folder = std::env::temp_dir().join(format!("ely-picture-{}-{width}", std::process::id()));
const SHOT: &str = "Before
![shot](assets/wide%20shot.png)
";
/// A 200×100 picture in `source`, drawn in a `width` column.
fn drawn_picture(source: &'static str, width: f32, cx: &mut TestAppContext) -> gpui::Size<Pixels> {
let named = format!(
"ely-picture-{}-{width}-{}",
std::process::id(),
source.len()
);
let folder = std::env::temp_dir().join(named);
std::fs::create_dir_all(folder.join("assets")).expect("a folder");
let mut png = Vec::new();
image::RgbaImage::new(200, 100)
@@ -328,10 +338,7 @@ fn drawn_picture(width: f32, cx: &mut TestAppContext) -> gpui::Size<Pixels> {
std::fs::write(folder.join("assets/wide shot.png"), png).expect("the picture");
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Page {
source: "Before
![shot](assets/wide%20shot.png)
",
source,
base: folder.clone(),
width,
});
@@ -350,13 +357,22 @@ fn drawn_picture(width: f32, cx: &mut TestAppContext) -> gpui::Size<Pixels> {
#[gpui::test]
fn a_picture_beside_the_file_draws_at_its_ratio_within_the_column(cx: &mut TestAppContext) {
let shrunk = drawn_picture(120.0, cx);
let shrunk = drawn_picture(SHOT, 120.0, cx);
assert_eq!(shrunk, size(px(120.0), px(60.0)), "shrunk at 2:1");
}
#[gpui::test]
fn a_picture_narrower_than_the_column_keeps_its_size(cx: &mut TestAppContext) {
assert_eq!(drawn_picture(400.0, cx), size(px(200.0), px(100.0)));
assert_eq!(drawn_picture(SHOT, 400.0, cx), size(px(200.0), px(100.0)));
}
#[gpui::test]
fn a_picture_in_a_table_cell_draws(cx: &mut TestAppContext) {
let table = "| Picture |
| --- |
| ![shot](assets/wide%20shot.png) |
";
assert_eq!(drawn_picture(table, 400.0, cx), size(px(200.0), px(100.0)));
}
#[gpui::test]
+28 -9
View File
@@ -16,7 +16,7 @@ pub enum Style {
Footnote,
}
/// Inline content: its text, the styled stretches of it, and the address of each link, in order.
/// Inline text, its styled stretches and link addresses.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Inline {
pub text: String,
@@ -34,15 +34,16 @@ pub enum Node {
language: Option<String>,
text: String,
},
/// Items with their blocks, numbered from `start` when ordered, each with its checkbox when a task.
/// Items and blocks; ordered from `start`; tasks carry checkboxes.
List {
start: Option<u64>,
items: Vec<(Option<bool>, Vec<Node>)>,
},
/// Each cell's blocks: its text, split at pictures.
Table {
aligns: Vec<AlignKind>,
head: Vec<Inline>,
rows: Vec<Vec<Inline>>,
head: Vec<Vec<Node>>,
rows: Vec<Vec<Vec<Node>>>,
},
Rule,
Math(String),
@@ -174,7 +175,12 @@ impl<'a> Reader<'a> {
mdast::Node::TableRow(row) => row
.children
.iter()
.map(|cell| self.inline(cell.children().expect("a cell holds inlines")))
.map(|cell| {
let mut blocks = Vec::new();
let inlines = cell.children().expect("a cell holds inlines");
self.paragraph(cell, inlines, &mut blocks);
blocks
})
.collect::<Vec<_>>(),
other => unreachable!("a table holds rows, not {other:?}"),
});
@@ -469,13 +475,26 @@ mod tests {
panic!("a table, not {:?}", nodes[0])
};
assert_eq!(aligns, &[AlignKind::Left, AlignKind::Right]);
let text = |cell: &[Node]| match cell {
[Node::Paragraph(line)] => line.text.clone(),
other => panic!("one line, not {other:?}"),
};
assert_eq!(
head.iter()
.map(|cell| cell.text.as_str())
.collect::<Vec<_>>(),
head.iter().map(|cell| text(cell)).collect::<Vec<_>>(),
["Name", "Lift"]
);
assert_eq!(rows[0][1].text, "0.5");
assert_eq!(text(&rows[0][1]), "0.5");
}
#[test]
fn a_picture_in_a_cell_stays_a_picture() {
let nodes = parse("| Picture |\n| --- |\n| ![shot](assets/shot.png) |\n");
let Node::Table { rows, .. } = &nodes[0] else {
panic!("a table, not {:?}", nodes[0])
};
assert!(
matches!(&rows[0][0][..], [Node::Image { alt, url, .. }] if alt == "shot" && url == "assets/shot.png")
);
}
#[test]