From e1ae303a1f47f8e26096bf666b46a822eb9782d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 12 Oct 2026 02:53:34 -0400 Subject: [PATCH] ElyMark: the shell hears each load; the size limit follows the text (C14.4 r1) Co-Authored-By: Claude Opus 5.5 (1M context) --- Cargo.lock | 2 +- Cargo.toml | 2 +- agents.md | 2 +- apps/ely-mark/src/desk.rs | 2 +- apps/ely-mark/src/places.rs | 2 +- apps/ely-mark/src/view.rs | 36 +++++++++++------ apps/ely-mark/src/view/tests.rs | 72 +++++++++++++++++++++++++++++++-- scripts/fetch-references.sh | 2 +- tasks/reviews/C14.4-r1.md | 9 +++++ 9 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 tasks/reviews/C14.4-r1.md diff --git a/Cargo.lock b/Cargo.lock index aec4a1b..dcd2f8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2044,7 +2044,7 @@ dependencies = [ [[package]] name = "ely-gpui-component" version = "0.1.1" -source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=69b11d785238e914215b081cca8ec3eb2e7efdcf#69b11d785238e914215b081cca8ec3eb2e7efdcf" +source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=f59c9f7f500be3b42ea184e96f747549f006099d#f59c9f7f500be3b42ea184e96f747549f006099d" dependencies = [ "alacritty_terminal", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 353c665..cf2570c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ jiff = "0.2" anyhow = "1" directories = "6" dunce = "1" -ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "69b11d785238e914215b081cca8ec3eb2e7efdcf" } +ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "f59c9f7f500be3b42ea184e96f747549f006099d" } env_logger = "0.11" gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] } gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit", "wayland", "x11"] } diff --git a/agents.md b/agents.md index fb68294..26c168c 100644 --- a/agents.md +++ b/agents.md @@ -40,7 +40,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木 ## 组件映射 -依赖 `ely-gpui-component`(pin 69b11d7,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。 +依赖 `ely-gpui-component`(pin f59c9f7,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。 | 需要 | 用 | | --- | --- | diff --git a/apps/ely-mark/src/desk.rs b/apps/ely-mark/src/desk.rs index 7035f62..84197e6 100644 --- a/apps/ely-mark/src/desk.rs +++ b/apps/ely-mark/src/desk.rs @@ -424,7 +424,7 @@ fn large_banner( view: &Entity, cx: &mut Context, ) -> Option { - if !view.read(cx).large { + if !view.read(cx).large() { return None; } let size = view.read(cx).bytes() as f64 / (1024.0 * 1024.0); diff --git a/apps/ely-mark/src/places.rs b/apps/ely-mark/src/places.rs index 1ede9f2..a31143f 100644 --- a/apps/ely-mark/src/places.rs +++ b/apps/ely-mark/src/places.rs @@ -108,7 +108,7 @@ impl Desk { } } - /// Line `line`, from zero, in `tab`, else in `file` once open. + /// Line `line` in `tab`, or in `file` once open. pub fn open_at( &mut self, tab: Option, diff --git a/apps/ely-mark/src/view.rs b/apps/ely-mark/src/view.rs index e398db8..17f8e3d 100644 --- a/apps/ely-mark/src/view.rs +++ b/apps/ely-mark/src/view.rs @@ -1,4 +1,4 @@ -//! One Markdown file in a document, its text kept for the shell. +//! One Markdown file, its text kept for the shell. use std::path::PathBuf; @@ -12,7 +12,7 @@ use gpui::{ use crate::engine::{Doc, mark}; -/// The text changed, or a new read of it is in. +/// The text changed, or a read is in. pub struct Reported; /// What went wrong, for the shell to say. @@ -58,7 +58,7 @@ pub struct Heading { pub line: usize, } -/// Where the reader was: the caret, and the first line in view. +/// The caret and the first line in view. #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Place { pub line: u64, @@ -78,8 +78,6 @@ pub struct MarkView { pub mode: Mode, /// The file's folder: pictures resolve here. pub(crate) folder: PathBuf, - /// Too long to read: source alone, every byte saved (MARK-14). - pub large: bool, pub focus: FocusHandle, } @@ -95,9 +93,10 @@ impl MarkView { cx: &mut Context, ) -> Self { let text = doc.lock().expect("the text lock").current.clone(); - // Past what the document reads: source alone (PERF-07). - let large = text.len() > LARGEST_READ; - let mode = if large { Mode::Source } else { Mode::Visual }; + let mode = match text.len() > LARGEST_READ { + true => Mode::Source, + false => Mode::Visual, + }; let view = cx.weak_entity(); let document = cx.new(|cx| { let mut document = MarkdownDocument::new(text, mode.shown(), window, cx); @@ -107,7 +106,9 @@ impl MarkView { }); document }); - cx.subscribe(&document, Self::heard).detach(); + cx.subscribe_in(&document, window, Self::heard).detach(); + // The shell hears the load. + cx.defer_in(window, |_, _, cx| cx.emit(Reported)); let focus = cx.focus_handle(); // The view's focus is the document's. cx.on_focus(&focus, window, |view, window, cx| { @@ -122,7 +123,6 @@ impl MarkView { saved, mode, folder, - large, focus, } } @@ -141,6 +141,11 @@ impl MarkView { self.doc.lock().expect("the text lock").current.len() } + /// Too long to read: source alone (MARK-14). + pub fn large(&self) -> bool { + self.bytes() > LARGEST_READ + } + pub fn modified(&self) -> bool { mark(&self.doc.lock().expect("the text lock").current) != self.saved } @@ -152,7 +157,7 @@ impl MarkView { /// The same text in `mode` (MARK-02). pub fn switch(&mut self, mode: Mode, window: &mut Window, cx: &mut Context) { - if self.large && mode != Mode::Source { + if self.large() && mode != Mode::Source { log::info!("mark: a large file stays in source"); let said = ely_ui::i18n::text(cx, "mark.large.modes", &[]).to_string(); return cx.emit(Failed(said)); @@ -228,7 +233,7 @@ impl MarkView { self.document .update(cx, |document, cx| document.reset(text, cx)); } - let mode = if self.large { Mode::Source } else { mode }; + let mode = if self.large() { Mode::Source } else { mode }; self.switch(mode, window, cx); if let Some(place) = place { self.document.update(cx, |document, cx| { @@ -250,14 +255,19 @@ impl MarkView { fn heard( &mut self, - document: Entity, + document: &Entity, event: &DocumentEvent, + window: &mut Window, cx: &mut Context, ) { if *event == DocumentEvent::Changed { let text = document.read(cx).text(cx); self.doc.lock().expect("the text lock").current = text; self.changed += 1; + if self.large() && self.mode != Mode::Source { + log::info!("mark: the text grew past the read limit"); + self.switch(Mode::Source, window, cx); + } } cx.emit(Reported); } diff --git a/apps/ely-mark/src/view/tests.rs b/apps/ely-mark/src/view/tests.rs index 3c78f93..9f987c0 100644 --- a/apps/ely-mark/src/view/tests.rs +++ b/apps/ely-mark/src/view/tests.rs @@ -1,8 +1,8 @@ use std::sync::{Arc, Mutex}; -use gpui::{Entity, TestAppContext, VisualTestContext}; +use gpui::{AppContext as _, Entity, TestAppContext, VisualTestContext}; -use super::{MarkView, Mode}; +use super::{MarkView, Mode, Reported}; use crate::engine::{Text, mark}; use crate::text::Source; @@ -48,14 +48,78 @@ fn typing_reaches_the_text_the_shell_saves(cx: &mut TestAppContext) { assert!(modified && changed > 0); } +fn current(view: &Entity, cx: &mut VisualTestContext) -> String { + view.read_with(cx, |view, _| { + view.doc.lock().expect("the text lock").current.clone() + }) +} + #[gpui::test] fn a_refresh_is_saved_text_and_keeps_no_undo(cx: &mut TestAppContext) { let (view, cx) = open("one\n", cx); - let disk = Source::decode(b"two\n".to_vec()).expect("reads"); + view.update(cx, |view, cx| view.adopt("edited\n".into(), cx)); + cx.run_until_parked(); + assert!(view.read_with(cx, |view, _| view.modified())); + let disk = Source::decode(b"from disk\n".to_vec()).expect("reads"); view.update(cx, |view, cx| view.refresh(disk, cx)); cx.run_until_parked(); + assert_eq!(current(&view, cx), "from disk\n"); assert!(!view.read_with(cx, |view, _| view.modified())); - assert_eq!(view.read_with(cx, |view, _| view.bytes()), 4); + view.update_in(cx, |view, window, cx| view.switch(Mode::Source, window, cx)); + let focus = view.read_with(cx, |view, _| view.focus.clone()); + cx.update(|window, cx| window.focus(&focus, cx)); + cx.run_until_parked(); + cx.simulate_keystrokes("secondary-z"); + cx.run_until_parked(); + assert_eq!(current(&view, cx), "from disk\n", "undo stops at the disk"); +} + +#[gpui::test] +fn the_shell_hears_a_load_it_subscribed_to(cx: &mut TestAppContext) { + cx.update(ely_gpui_component::init_for_tests); + cx.update(|cx| { + ely_ui::settings::install_defaults(cx); + ely_ui::i18n::install(cx); + }); + let heard = std::rc::Rc::new(std::cell::Cell::new(0)); + let (count, held) = ( + heard.clone(), + std::rc::Rc::new(std::cell::RefCell::new(None)), + ); + let holder = held.clone(); + let (_root, cx) = cx.add_window_view(move |window, cx| { + let source = Source::decode(b"# Opened\n".to_vec()).expect("reads"); + let saved = mark(&source.text); + let doc = Arc::new(Mutex::new(Text::new(source))); + let folder = std::env::temp_dir(); + let view = cx.new(|cx| MarkView::new(doc, saved, folder, window, cx)); + // As the shell: subscribed after the view is made. + let count = count.clone(); + cx.subscribe(&view, move |_, _, _: &Reported, _| { + count.set(count.get() + 1) + }) + .detach(); + holder.replace(Some(view)); + gpui::Empty + }); + cx.run_until_parked(); + assert!(heard.get() > 0); +} + +#[gpui::test] +fn a_text_grown_past_the_limit_goes_to_source(cx: &mut TestAppContext) { + let (view, cx) = open("# Old\n", cx); + let long = "x".repeat(ely_gpui_component::documents::LARGEST_READ + 1); + view.update(cx, |view, cx| view.adopt(long, cx)); + cx.run_until_parked(); + assert!(view.read_with(cx, |view, _| view.large())); + assert_eq!(view.read_with(cx, |view, _| view.mode), Mode::Source); + view.update_in(cx, |view, window, cx| { + view.switch(Mode::Preview, window, cx) + }); + assert_eq!(view.read_with(cx, |view, _| view.mode), Mode::Source); + let outline = view.read_with(cx, |view, cx| view.outline(cx)); + assert!(outline.is_empty(), "no stale heading: {outline:?}"); } #[gpui::test] diff --git a/scripts/fetch-references.sh b/scripts/fetch-references.sh index 7318acb..295590a 100755 --- a/scripts/fetch-references.sh +++ b/scripts/fetch-references.sh @@ -36,7 +36,7 @@ fetch() { # name url rev [sparse paths...] echo "fetch $name $rev" } -fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git 69b11d785238e914215b081cca8ec3eb2e7efdcf +fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git f59c9f7f500be3b42ea184e96f747549f006099d fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \ include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \ diff --git a/tasks/reviews/C14.4-r1.md b/tasks/reviews/C14.4-r1.md new file mode 100644 index 0000000..a94c8f2 --- /dev/null +++ b/tasks/reviews/C14.4-r1.md @@ -0,0 +1,9 @@ +FAIL + +- [P1] apps/ely-mark/src/view.rs:110 — No initial `Reported` reaches the shell for files parsed synchronously (up to 128 KiB), or files above `LARGEST_READ`. `MarkdownDocument::new` emits its synchronous parse event before this subscription activates, and the view supplies no later initialization event. Opening `# Opened\n` without editing leaves `Desk::observed` uncalled: the recent list stays empty, the file has no watcher, `extra.base` stays empty, and restoration never starts. The first edit also cannot initialize the comparison baseline because the tab is already dirty. A GPUI probe through `Desk::open_file` reproduced `(recent=0, watcher=false, restored=false, base="")`; see `.work/C14.4-r1-probes.log`. Initialize each loaded tab once after the engine subscriptions and tab registration are ready, and test these initial file-session effects. + +- [P2] apps/ely-mark/src/view.rs:99 — `large` is computed only at construction, although component commit 69b11d7 stops parsing whenever the current text exceeds 2 MiB (`Ely-GPUI-Components/src/documents/document/mod.rs:238`) and retains the previous parsed blocks. Open `# Old`, switch to source, select all, paste 2,097,153 bytes of `x`, then switch to preview: the switch succeeds and the preview/outline still show `Old`, which is absent from the current file. The clipboard-paste GPUI probe reproduced this; see `.work/C14.4-r1-paste.log`. Enforce the source-only limit against the current text on changes/replacements and mode switches, invalidate stale parsed content at the cutoff, and cover crossing the threshold after opening. + +- [P2] apps/ely-mark/src/view/tests.rs:52 — `a_refresh_is_saved_text_and_keeps_no_undo` verifies neither the replacement text nor undo reset. Both its old and new strings have four bytes, and the initial view is already unmodified, so replacing the entire `MarkView::refresh` body with a no-op still passes this test (verified in the isolated copy; `.work/C14.4-r1-refresh-mutant.log`). Assert the exact document and shell text after refresh, begin with an actual undoable edit, and exercise undo to prove the disk text cannot revert to the preceding content. + +- [P3] apps/ely-mark/src/view.rs:1 — New comments violate the ten-word limit: this module comment has 12 words, line 15 has 11, line 61 has 12, and apps/ely-mark/src/places.rs:111 has 11. Running `python tests/scripts/comments.py` fails on exactly these four locations. Shorten each comment to at most ten words.