ElyMark: the print page is staged off the window thread (M-14)

paper::stage takes the background executor and inlines images, reads
the style sheet and writes the page there. A gpui test checks nothing
is written before the executor runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-10 14:14:56 -04:00
1 parent ca5ac5bb45
commit 6247771f24
5 files changed
+62 -15

No files matched your search

+10 -14
View File
@@ -15,7 +15,7 @@ use ely_ui::{commands, i18n::text};
use ely_web_content::{Event, WebView};
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, IntoElement as _, ParentElement as _,
Subscription, Window,
Subscription, Task, Window,
};
use serde_json::Value;
@@ -62,7 +62,7 @@ pub struct Lost {
}
impl Lost {
fn of(images: Vec<String>, rendered: &Value) -> Lost {
pub fn of(images: Vec<String>, rendered: &Value) -> Lost {
let diagrams = rendered["failed"].as_array().into_iter().flatten();
let formulas = rendered["formulas"].as_array().into_iter().flatten();
Lost {
@@ -280,19 +280,15 @@ impl Desk {
"html",
move |rendered, cx: &mut App| {
let staged = work.join(format!("print-{n}.html"));
let writing = staged.clone();
let page = match rendered {
Ok(rendered) => {
let print = paper::Print { folder, style, name, page: staged.clone() };
paper::stage(cx.background_executor(), rendered, print)
}
Err(why) => Task::ready(Err(io::Error::other(why))),
};
cx.spawn(async move |cx| {
// Images inline and the page writes off the window thread.
let page = cx
.background_spawn(async move {
rendered.map_err(io::Error::other).and_then(|rendered| {
let (html, images) = paper::Marked::read(&rendered)?.inline(&folder);
let style = format!("<style>{}</style>", std::fs::read_to_string(&style)?);
std::fs::write(&writing, paper::page(&name, &style, &html))?;
Ok(Lost::of(images, &rendered))
})
})
.await;
let page = page.await;
in_window(&desk, cx, |desk, window, cx| match page {
Ok(lost) => {
let job = Job {
+47
View File
@@ -85,6 +85,28 @@ fn kind(path: &Path) -> &'static str {
}
}
/// What the print page is made of.
pub struct Print {
pub folder: std::path::PathBuf,
pub style: std::path::PathBuf,
pub name: String,
pub page: std::path::PathBuf,
}
/// Writes the print page off the window thread, images inline.
pub fn stage(
background: &gpui::BackgroundExecutor,
rendered: serde_json::Value,
print: Print,
) -> gpui::Task<std::io::Result<crate::export::Lost>> {
background.spawn(async move {
let (html, images) = Marked::read(&rendered)?.inline(&print.folder);
let style = format!("<style>{}</style>", std::fs::read_to_string(&print.style)?);
std::fs::write(&print.page, page(&print.name, &style, &html))?;
Ok(crate::export::Lost::of(images, &rendered))
})
}
/// The page's HTML; image `src` values replaced by numbered tokens.
#[derive(Clone, Debug, Default)]
pub struct Marked {
@@ -222,6 +244,31 @@ mod tests {
assert!(Marked::read(&serde_json::json!({ "html": "<p>" })).is_err());
}
#[gpui::test]
fn the_print_page_is_written_off_the_window_thread(cx: &mut gpui::TestAppContext) {
let dir = tempfile::tempdir().expect("a temp dir");
std::fs::write(dir.path().join("a.png"), b"png").expect("an image");
std::fs::write(dir.path().join("export.css"), "p {}").expect("a style");
let rendered = serde_json::json!({
"html": "<p>hi</p><img src=\"T0\"><img src=\"T1\">",
"token": "T",
"images": ["a.png", "gone.png"],
"failed": [3],
});
let print = Print {
folder: dir.path().into(),
style: dir.path().join("export.css"),
name: "notes".into(),
page: dir.path().join("print-1.html"),
};
let staging = stage(&cx.executor(), rendered, print);
assert!(!dir.path().join("print-1.html").exists(), "staged on the window thread");
let lost = cx.foreground_executor().block_test(staging).expect("staged");
let written = std::fs::read_to_string(dir.path().join("print-1.html")).expect("the page");
assert!(written.contains("<style>p {}</style>") && written.contains("data:image/png;base64,cG5n"));
assert_eq!(format!("{lost:?}"), r#"Lost { images: ["gone.png"], diagrams: [3], formulas: [] }"#);
}
#[test]
fn titles_escape() {
assert!(page("a<b>&", "", "").contains("<title>a&lt;b&gt;&amp;</title>"));
+1 -1
View File
@@ -74,7 +74,7 @@
- [x] M-11 页面把搜索词写进应用日志 — 控制台的文字与来源都可由文档伪造,只记行号;页面自己的诊断经 elyQuery;R1 前缀可伪造、R2 data 来源、R3 sourceURL、R4 编码路径与目录、R5 缺回归测试:已补并经变异核对(五轮上限,未再审)
- [x] M-12 浏览器创建完成前 `WebView` 已释放,浏览器泄漏 — 浏览器在泵里同步建好即登记,视图要么在建前已走、要么有浏览器可关;webprobe 加建前丢弃与 0–27 毫秒丢弃,35 页都在保持前释放(审查 4 轮)
- [x] M-13 编辑器构建失败时页面空白且不上报 — 可视建不成退回源码并告知,两者都建不成即报失败、留下文字、模式请求重建、不留容器;新测 mark-broken.mjs(审查 2 轮)
- [ ] M-14 `render_pdf` 在界面线程内嵌图片并写文件
- [x] M-14 `render_pdf` 在界面线程内嵌图片并写文件 — 打印页的内嵌、读样式与写入经 paper::stage 在后台执行,GPUI 单测证明执行器运行前未写(审查 2 轮)
- [ ] M-15 打印与导出 PDF 的暂存页执行文档里的脚本
- [ ] G-01 打开“文件历史”或“逐行追溯”后,中间区不再响应任何别的选择
- [ ] G-02 进行中操作读取失败被吞掉;`git am` 冲突无提示条无错误
+3
View File
@@ -0,0 +1,3 @@
FAIL
apps/ely-mark/src/export.rs:287 — 缺少 M-14 的后台暂存回归测试。具体失败场景:把此处 `background_spawn(...).await` 改回前台直接执行,图片内嵌、样式读取及暂存页写入重新阻塞界面,现有测试仍全绿。本轮在隔离副本中编译并运行该变异:Windows 的 22 项适用测试、WSL 的全部 24 项测试均通过;现有 `mark-output.sh` 只检查输出及打印作业,没有线程或界面响应断言。额外的 GPUI 暂存片段探针在当前代码上通过六种成功、失败场景,在该变异上明确报出 `staging ran on foreground`,证明现有测试漏掉了本项针对的行为(UX-05、PERF-02)。修复方向:补充执行实际暂存调度路径的 GPUI 回归测试,验证耗时准备在后台执行、结果交回前台处理,撤掉后台调度时必须失败。证据:`.work/M-14-r1/{windows,linux}-foreground-mutant-tests.log`、`{windows,linux}-staging-{current,foreground-mutant}-tests.log`。探针副本及其独立构建目录已删除;未运行 macOS 窗口测试。
+1
View File
@@ -0,0 +1 @@
PASS