From 3d20036bff0bbac4a7f6dd29a703afc40025fa46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 10 Oct 2026 00:02:40 -0400 Subject: [PATCH] ElyPDF: a preview overtaken by an edit says so (P-10) --- apps/ely-pdf/src/tests/editing.rs | 30 ++++++++++++++++++++++++++++++ apps/ely-pdf/src/writes.rs | 8 +++++++- tasks/BUGS.md | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/ely-pdf/src/tests/editing.rs b/apps/ely-pdf/src/tests/editing.rs index b089149..0aba53c 100644 --- a/apps/ely-pdf/src/tests/editing.rs +++ b/apps/ely-pdf/src/tests/editing.rs @@ -397,3 +397,33 @@ fn a_small_picture_on_text_under_a_page_form_still_aims() { click(cx, &view, (140.0, 66.0)); assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Text { .. })), "the text, not the form around it"); } + +#[test] +fn a_preview_overtaken_by_an_edit_says_so() { + use ely_gpui_component::primitives::Severity; + let dir = tempfile::tempdir().expect("a temp dir"); + let mut cx = TestAppContext::single(); + let (viewer, cx, view) = editing(&mut cx, dir.path()); + // The window's toasts live on its root. + let root = cx.update(|window, app| { + let root = ely_ui::document_root(ely_platform::AppId::Pdf, viewer.clone().into(), &ely_ui::commands::FILE, Vec::new(), window, app); + viewer.update(app, |viewer, _| viewer.core.root = root.downgrade()); + root + }); + click(cx, &view, (80.0, 66.0)); + type_in(cx, &viewer, "Hi world\nSecond line"); + cx.update(|_, app| viewer.update(app, |viewer, cx| { + // A new font asks for a preview first. + viewer.edit_panel.update(cx, |panel, _| panel.font = "times".into()); + viewer.write_text(false, cx); + // An edit lands before the preview answers. + if let Some(view) = viewer.active_view() { + view.update(cx, |view, _| view.version += 1); + } + })); + let changed = cx.update(|_, app| Viewer::text(app, "pdf.object.changed", &[])); + settle(cx, |app| root.read(app).toasts(app).iter().any(|(text, severity)| *severity == Some(Severity::Warning) && *text == changed)); + frames(cx); + assert!(cx.update(|_, app| viewer.read(app).preview.is_none()), "no stale preview"); + assert_eq!(cx.update(|_, app| view.read(app).edited), 0, "nothing written"); +} diff --git a/apps/ely-pdf/src/writes.rs b/apps/ely-pdf/src/writes.rs index d09674c..6787e28 100644 --- a/apps/ely-pdf/src/writes.rs +++ b/apps/ely-pdf/src/writes.rs @@ -71,7 +71,8 @@ impl Viewer { let (asked, viewer) = (writing.clone(), cx.weak_entity()); view.update(cx, |view, cx| { let doc = view.doc; - view.read( + // A stale answer still reaches `previewed`, which says so. + view.off( move |worker| worker.put_text(doc, page, &asked, PREVIEW), move |_, answer, cx| { // The viewer edits this view; wait until it is free. @@ -95,6 +96,11 @@ impl Viewer { replacing: bool, cx: &mut Context, ) { + let same = self.tab(tab).is_some_and(|open| open.view.read(cx).version == version); + if !same { + let text = Self::text(cx, "pdf.object.changed", &[]); + return self.toast(text, Severity::Warning, cx); + } match answer { Ok(Wrote::Unwritable) => { let note = Self::text(cx, "pdf.text.unwritable", &[]); diff --git a/tasks/BUGS.md b/tasks/BUGS.md index b08489a..a9c5adc 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -42,7 +42,7 @@ - [x] P-07 含连字的文字块永远不可编辑 — 字形按 stext 的写法展开(多字字形、连字码点),整页、单块与删除同一判法;R3 PASS - [x] P-08 页面读取失败后每帧重试、每帧弹提示 — 失败的读取按页或种类记下,编辑后重读;签名读取以保存计数判过期 - [x] P-09 识别文字的写入被拒后仍弹“成功” — on_same 回报是否执行;被拒时只有警告 -- [ ] P-10 预览回复被丢弃时“应用”静默无事 +- [x] P-10 预览回复被丢弃时“应用”静默无事 — 预览回复照常交回,版本已变即提示,不显示过期预览 - [ ] P-11 撤销或重做在 MuPDF 侧失败后 worker 的步数不再同步 - [ ] P-12 `unhook` 中途抛出时泄漏保留的对象 - [ ] P-13 优化导出的“之前”大小取磁盘文件而非当前文档