diff --git a/apps/ely-mark/src/export.rs b/apps/ely-mark/src/export.rs index afcb4bf..c0f18ba 100644 --- a/apps/ely-mark/src/export.rs +++ b/apps/ely-mark/src/export.rs @@ -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, rendered: &Value) -> Lost { + pub fn of(images: Vec, 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!("", 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 { diff --git a/apps/ely-mark/src/paper.rs b/apps/ely-mark/src/paper.rs index fbe5188..8eb74de 100644 --- a/apps/ely-mark/src/paper.rs +++ b/apps/ely-mark/src/paper.rs @@ -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> { + background.spawn(async move { + let (html, images) = Marked::read(&rendered)?.inline(&print.folder); + let style = format!("", 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": "

" })).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": "

hi

", + "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("") && 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&", "", "").contains("a<b>&")); diff --git a/tasks/BUGS.md b/tasks/BUGS.md index df0648e..388461a 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -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` 冲突无提示条无错误 diff --git a/tasks/reviews/M-14-r1.md b/tasks/reviews/M-14-r1.md new file mode 100644 index 0000000..a5b3116 --- /dev/null +++ b/tasks/reviews/M-14-r1.md @@ -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 窗口测试。 diff --git a/tasks/reviews/M-14-r2.md b/tasks/reviews/M-14-r2.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/M-14-r2.md @@ -0,0 +1 @@ +PASS