documents: pictures draw in table cells; Markdown-path comments within ten words
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:
1 parent
bbb607106a
commit
4801555e0d
6 files changed
+84
-43
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||

|
||||
";
|
||||
|
||||
/// 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
|
||||
|
||||

|
||||
",
|
||||
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 |
|
||||
| --- |
|
||||
|  |
|
||||
";
|
||||
assert_eq!(drawn_picture(table, 400.0, cx), size(px(200.0), px(100.0)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
+28
-9
@@ -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|  |\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]
|
||||
|
||||
Reference in new issue
Block a user