diff --git a/agents.md b/agents.md index 9b1bd72..274e6f8 100644 --- a/agents.md +++ b/agents.md @@ -283,4 +283,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-11:Linux 包的实测(L1.6)。本机(Omarchy,Hyprland 0.56)装上六个 Arch 包与六个 AppImage,`linux-installed.sh` 两种都过;deb、rpm、Arch 包在 Ubuntu 26.04、Fedora 44、Arch 容器里装上、全部 ELF 找得到库;deb 与 rpm 的六个应用在各自容器的 Xvfb 下画出窗口(`linux-distros.sh`)。截图按窗口(`grim -T` 加 Hyprland 的 `stableId`):按区域截时用户切换工作区或别的窗口盖住即截错。ElyMark 的文件监视略去读取后不会自激:重读前先比指纹(长度与修改时间),不打开文件。 - 2026-10-11(用户定:替换 v0.1.3 的 Linux 包):v0.1.3 发布里原有的六个 AppImage(无头,打不开窗口)已删去,换成本机容器里建出的六个应用各四种包(共 24 个),出自 L1.7 的提交(晚于该标签);上传后逐个核对大小,ElyTree 的 AppImage 下载回来散列相同。`package.sh` 把每个 RUNPATH 按所在 ELF 解析(`$ORIGIN` 或 `${ORIGIN}` 开头,规范化后须在 AppDir 内),否则拒绝(如未修正的 cairo):CI 用的是别处建好的引擎,旧构建会在这里报错而不是带进包里。替换的 Linux 包对应的源码随之上传到同一发布(`source.sh`,文件名带构建提交号),发布说明写明 Linux 包出自哪个提交。 - 2026-10-11:`ci.sh` 用 `ELY_SHARE` 时,CI 克隆里的 `engines`、`reference` 须是指向共享克隆的链接(Windows 为目录联接,不查);是本克隆自建的目录或指向别处即报错,不混用。`.gitignore` 的 `/engines`、`/reference` 不带斜杠,链接也被忽略(`source.sh` 要求工作区干净)。 -- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印删去,在 C14.9、C14.10、C14.12、C14.14 原生重做。C14.5 删去 CEF:`crates/ely-web-content`、`apps/ely-mark/web`、`scripts/fetch-cef.sh`、`scripts/build-mark-content.sh`、macOS 包里的辅助应用与其授权、Linux AppImage 的 `libcef` 安排、CEF 与网页依赖的许可、存储设置里的网页缓存、`webview.sh`、`hardened-macos.sh`、`linux-mark-idle.sh`、参考源码 cef、cef-rs、milkdown、codemirror-dev;`apps::open` 不再复原信号(只为 Chromium 屏蔽的 SIGTERM 而设)。旧 C14.1 至 C14.7、R2.13 与 ElyMark AppImage 的记录(CEF 实现)删去,在 git 历史;仍在的应用行为(外部变更与比较、会话、改名、Git 状态、最近与文件夹、`look.json`)在 C14.8 至 C14.16 逐条核对后重记。用户以前的 ElyMark 数据目录里可能留着 `web` 缓存文件夹,Ely 不再读写它。组件库 6f0401e:焦点进入空的可视文档即打开末尾的输入框,新文件可直接键入(原先键入无处落下);362a0fc:换回可视时同样打开(预览与可视共用焦点,无焦点事件),全文撤销后若文档成空,读完新文字再打开末尾的输入框;639521e:只有一份撤销历史,块的输入框自己的撤销与重做从不运行:在打开的块里撤销或重做,先把块写回为一步,再走全文的历史(块内逐键的撤销没有了,一次块编辑是一步),重做跨模式、跨多次撤销都在。ElyMark(C14.6):每个标签各记其模式,切换不改一个字节(编码写出与原文相同),撤销跨模式(全文一份撤销历史);视图在模式首次可键入时记一次 `mark: shown as "…"`(源码即刻,可视与预览在块读完后),PERF-07 计到这条。Windows 上实测(Ryzen 9 9950X3D)1 MB 文档 P50 341 毫秒、P95 560 毫秒(`docs/perf/2026-10-12-Ryzen9-9950X3D.md`);macOS、Linux 与树莓派未测。C14.7:语法由 markdown-rs 读(CommonMark 加 GFM 的表格、任务、删除线、自动链接,脚注,公式),front matter 不进可视,以原文块显示在最上(组件库 parse 测试核对各种块画成内容而非原文);链接照 M-05:预览单击、可视 Cmd(他处 Ctrl)按下交应用,可视普通按下编辑该块(组件库 bbb6071 的链接测试);应用只把 http、https、mailto 交系统打开,其余以提示拒绝(应用测试);链接地址与图片读不出的原因不进日志(地址与路径属文档内容)。文档旁图片按文件所在文件夹读(`MarkView` 在打开与改名时交给组件);组件库 4801555:表格单元格也是块,图片在格内照常画出(原只画替代文字),图片取自身宽度、以所在容器为上限(单元格可收缩,窄格里的图片按比例缩小,组件库 2e196a1)。gpui 的图片加载(`vendor/gpui-pre` 的 `img.rs`):GIF 与 WebP 的坏帧即结束加载并报错(`img.rs` 的加载器与 `platform.rs` 的 `Image::to_image_data`,后者给 `data:` 图片用)(原样跳过坏帧,解码器到文件尾仍不停地报错,加载永不结束、占满一个核),错误与日志不带图片路径(原样把路径写进错误,`AssetLogger` 照写进日志);应用测试以坏 GIF 核对两处加载都结束、错误里没有所在文件夹名。 +- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印删去,在 C14.9、C14.10、C14.12、C14.14 原生重做。C14.5 删去 CEF:`crates/ely-web-content`、`apps/ely-mark/web`、`scripts/fetch-cef.sh`、`scripts/build-mark-content.sh`、macOS 包里的辅助应用与其授权、Linux AppImage 的 `libcef` 安排、CEF 与网页依赖的许可、存储设置里的网页缓存、`webview.sh`、`hardened-macos.sh`、`linux-mark-idle.sh`、参考源码 cef、cef-rs、milkdown、codemirror-dev;`apps::open` 不再复原信号(只为 Chromium 屏蔽的 SIGTERM 而设)。旧 C14.1 至 C14.7、R2.13 与 ElyMark AppImage 的记录(CEF 实现)删去,在 git 历史;仍在的应用行为(外部变更与比较、会话、改名、Git 状态、最近与文件夹、`look.json`)在 C14.8 至 C14.16 逐条核对后重记。用户以前的 ElyMark 数据目录里可能留着 `web` 缓存文件夹,Ely 不再读写它。组件库 6f0401e:焦点进入空的可视文档即打开末尾的输入框,新文件可直接键入(原先键入无处落下);362a0fc:换回可视时同样打开(预览与可视共用焦点,无焦点事件),全文撤销后若文档成空,读完新文字再打开末尾的输入框;639521e:只有一份撤销历史,块的输入框自己的撤销与重做从不运行:在打开的块里撤销或重做,先把块写回为一步,再走全文的历史(块内逐键的撤销没有了,一次块编辑是一步),重做跨模式、跨多次撤销都在。ElyMark(C14.6):每个标签各记其模式,切换不改一个字节(编码写出与原文相同),撤销跨模式(全文一份撤销历史);视图在模式首次可键入时记一次 `mark: shown as "…"`(源码即刻,可视与预览在块读完后),PERF-07 计到这条。Windows 上实测(Ryzen 9 9950X3D)1 MB 文档 P50 341 毫秒、P95 560 毫秒(`docs/perf/2026-10-12-Ryzen9-9950X3D.md`);macOS、Linux 与树莓派未测。C14.7:语法由 markdown-rs 读(CommonMark 加 GFM 的表格、任务、删除线、自动链接,脚注,公式),front matter 不进可视,以原文块显示在最上(组件库 parse 测试核对各种块画成内容而非原文);链接照 M-05:预览单击、可视 Cmd(他处 Ctrl)按下交应用,可视普通按下编辑该块(组件库 bbb6071 的链接测试);应用只把 http、https、mailto 交系统打开,其余以提示拒绝(应用测试);链接地址与图片读不出的原因不进日志(地址与路径属文档内容)。文档旁图片按文件所在文件夹读(`MarkView` 在打开与改名时交给组件);组件库 4801555:表格单元格也是块,图片在格内照常画出(原只画替代文字),图片取自身宽度、以所在容器为上限(单元格可收缩,窄格里的图片按比例缩小,组件库 2e196a1)。gpui 的图片加载(`vendor/gpui-pre` 的 `img.rs`):GIF 与 WebP 的坏帧即结束加载并报错(`img.rs` 的加载器与 `platform.rs` 的 `Image::to_image_data`,后者给 `data:` 图片用)(原样跳过坏帧,解码器到文件尾仍不停地报错,加载永不结束、占满一个核),错误与日志不带图片路径(原样把路径写进错误,`AssetLogger` 照写进日志);应用测试以坏 GIF 核对两处加载都结束、错误里没有所在文件夹名。C14.8:左栏三页照旧(文件夹树、大纲、搜索);大纲取组件按 markdown-rs 读出的标题与行,选一项即 `reveal`(该行在视图里、插入点在其上;可视模式里键入仍须先按下块);多标签与最近列表经外壳与 `recent.json` 照旧。`desk::open_in` 收数据文件夹(`open` 照旧取本机的),应用的 gpui 测试以临时文件夹起整个窗口:打开文件即有标签、大纲行号、跳转落点与最近列表,键入的标题进大纲,两个文件两个标签各有大纲,打开文件夹只列 Markdown;文件监视在真线程上,测试以 `allow_parking` 允许。`ely_ui::install_no_launch_files`(`test-support`)给测试一个空的启动文件队列。随删去的网页页面一起的 `web.*` 文案(`i18n/web.rs`)删去。 diff --git a/apps/ely-mark/src/desk.rs b/apps/ely-mark/src/desk.rs index 84197e6..1282359 100644 --- a/apps/ely-mark/src/desk.rs +++ b/apps/ely-mark/src/desk.rs @@ -145,8 +145,19 @@ impl Host for Desk { /// The window root; files given at launch open. pub fn open(app: AppId, window: &mut Window, cx: &mut App) -> Entity { + let data = ely_platform::data_dir(app) + .unwrap_or_else(|error| startup::fail(app, &format!("{error:#}"))); + open_in(app, data, window, cx).0 +} + +/// The window root and its desk, kept in `data`. +pub(crate) fn open_in( + app: AppId, + data: PathBuf, + window: &mut Window, + cx: &mut App, +) -> (Entity, Entity) { let fail = |error: String| -> ! { startup::fail(app, &error) }; - let data = ely_platform::data_dir(app).unwrap_or_else(|error| fail(format!("{error:#}"))); let work = ely_office::fresh_work(&data).unwrap_or_else(|error| fail(format!("{error}"))); crate::keys::bind(cx); let recent_file = data.join("recent.json"); @@ -274,7 +285,7 @@ pub fn open(app: AppId, window: &mut Window, cx: &mut App) -> Entity { desk.should_close(window, cx) }) }); - root + (root, desk) } impl Desk { @@ -391,30 +402,31 @@ impl Render for Desk { .status(self.status(cx)), } .child(body); - let root = div() - .size_full() - .key_context(commands::CONTEXT) - .track_focus(&self.core.focus) - .on_action(cx.listener(Self::open_picked)) - .on_action(cx.listener(Self::on_new)) - .on_action(cx.listener(Self::on_save)) - .on_action(cx.listener(Self::on_save_as)) - .on_action(cx.listener(Self::on_rename)) - .on_action(cx.listener(Self::on_close_tab)) - .on_action(cx.listener(|desk, _: &CheckDisk, _, cx| desk.look_all(cx))) - .on_action(cx.listener(|desk, _: &VisualMode, window, cx| { - desk.switch(Mode::Visual, window, cx) - })) - .on_action(cx.listener(|desk, _: &SourceMode, window, cx| { - desk.switch(Mode::Source, window, cx) - })) - .on_action(cx.listener(|desk, _: &PreviewMode, window, cx| { - desk.switch(Mode::Preview, window, cx) - })) - .child(shell) - .children(self.core_dialog(cx)) - .children(self.naming_element(cx)) - .children(self.compare_dialog(cx)); + let root = + div() + .size_full() + .key_context(commands::CONTEXT) + .track_focus(&self.core.focus) + .on_action(cx.listener(Self::open_picked)) + .on_action(cx.listener(Self::on_new)) + .on_action(cx.listener(Self::on_save)) + .on_action(cx.listener(Self::on_save_as)) + .on_action(cx.listener(Self::on_rename)) + .on_action(cx.listener(Self::on_close_tab)) + .on_action(cx.listener(|desk, _: &CheckDisk, _, cx| desk.look_all(cx))) + .on_action(cx.listener(|desk, _: &VisualMode, window, cx| { + desk.switch(Mode::Visual, window, cx) + })) + .on_action(cx.listener(|desk, _: &SourceMode, window, cx| { + desk.switch(Mode::Source, window, cx) + })) + .on_action(cx.listener(|desk, _: &PreviewMode, window, cx| { + desk.switch(Mode::Preview, window, cx) + })) + .child(shell) + .children(self.core_dialog(cx)) + .children(self.naming_element(cx)) + .children(self.compare_dialog(cx)); Self::look_actions(root, cx) } } @@ -435,3 +447,6 @@ fn large_banner( said, )) } + +#[cfg(test)] +mod tests; diff --git a/apps/ely-mark/src/desk/tests.rs b/apps/ely-mark/src/desk/tests.rs new file mode 100644 index 0000000..d92286f --- /dev/null +++ b/apps/ely-mark/src/desk/tests.rs @@ -0,0 +1,143 @@ +use std::{cell::RefCell, path::Path, rc::Rc}; + +use ely_platform::AppId; +use gpui::{Entity, TestAppContext, VisualTestContext}; + +use super::{Desk, open_in}; +use crate::view::Mode; + +/// ElyMark over a fresh data folder, as `main` opens it. +fn start(cx: &mut TestAppContext, data: &Path) -> (Entity, &'static mut VisualTestContext) { + // The file watcher runs on a real thread. + cx.executor().allow_parking(); + cx.update(ely_gpui_component::init_for_tests); + cx.update(|cx| { + ely_ui::settings::install_defaults(cx); + ely_ui::i18n::install(cx); + ely_ui::install_no_launch_files(cx); + }); + let held = Rc::new(RefCell::new(None)); + let kept = held.clone(); + let data = data.to_path_buf(); + let window = cx.update(|cx| { + cx.open_window(Default::default(), |window, cx| { + let (root, desk) = open_in(AppId::Mark, data, window, cx); + kept.replace(Some(desk)); + root + }) + .expect("a window") + }); + let cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + cx.run_until_parked(); + let desk = held.take().expect("the desk"); + (desk, cx) +} + +fn open(desk: &Entity, path: &Path, cx: &mut VisualTestContext) { + desk.update(cx, |desk, cx| desk.open_path(path.to_path_buf(), cx)); + cx.run_until_parked(); +} + +fn outline(desk: &Entity, cx: &mut VisualTestContext) -> Vec<(usize, String, usize)> { + desk.read_with(cx, |desk, cx| { + let view = desk.active_view().expect("a tab"); + let outline = view.read(cx).outline(cx); + outline + .into_iter() + .map(|heading| (heading.level, heading.title, heading.line)) + .collect() + }) +} + +#[gpui::test] +fn a_file_opens_with_its_outline_and_joins_the_recent_list(cx: &mut TestAppContext) { + let (data, files) = ( + tempfile::tempdir().expect("data"), + tempfile::tempdir().expect("files"), + ); + let path = files.path().join("a.md"); + std::fs::write(&path, "# One\n\nText.\n\n## Two\n\nMore.\n").expect("a file"); + let (desk, cx) = start(cx, data.path()); + open(&desk, &path, cx); + assert_eq!( + outline(&desk, cx), + [(1, "One".into(), 0), (2, "Two".into(), 4)] + ); + let view = desk.read_with(cx, |desk, _| desk.active_view().expect("a tab")); + view.update_in(cx, |view, window, cx| view.reveal(4, window, cx)); + cx.run_until_parked(); + assert_eq!( + view.read_with(cx, |view, cx| view.place(cx).line), + 4, + "the jump lands" + ); + let real = std::fs::canonicalize(&path).expect("real"); + assert!(desk.read_with(cx, |desk, _| desk.recent.contains(&real))); + let kept = std::fs::read_to_string(data.path().join("recent.json")).expect("the list"); + assert!(kept.contains("a.md"), "{kept}"); +} + +#[gpui::test] +fn the_outline_follows_typing(cx: &mut TestAppContext) { + let (data, files) = ( + tempfile::tempdir().expect("data"), + tempfile::tempdir().expect("files"), + ); + let path = files.path().join("a.md"); + std::fs::write(&path, "Text.\n").expect("a file"); + let (desk, cx) = start(cx, data.path()); + open(&desk, &path, cx); + let view = desk.read_with(cx, |desk, _| desk.active_view().expect("a tab")); + view.update_in(cx, |view, window, cx| view.switch(Mode::Source, window, cx)); + view.update(cx, |view, cx| view.adopt("# New\n\nText.\n".into(), cx)); + cx.executor() + .advance_clock(std::time::Duration::from_secs(1)); + cx.run_until_parked(); + assert_eq!(outline(&desk, cx), [(1, "New".into(), 0)]); +} + +#[gpui::test] +fn two_files_are_two_tabs_each_with_its_outline(cx: &mut TestAppContext) { + let (data, files) = ( + tempfile::tempdir().expect("data"), + tempfile::tempdir().expect("files"), + ); + let (a, b) = (files.path().join("a.md"), files.path().join("b.md")); + std::fs::write(&a, "# A\n").expect("a"); + std::fs::write(&b, "# B\n").expect("b"); + let (desk, cx) = start(cx, data.path()); + open(&desk, &a, cx); + open(&desk, &b, cx); + assert_eq!(desk.read_with(cx, |desk, _| desk.core.tabs.len()), 2); + assert_eq!(outline(&desk, cx), [(1, "B".into(), 0)]); + let first = desk.read_with(cx, |desk, _| desk.core.tabs[0].id); + desk.update(cx, |desk, cx| { + desk.core.active = Some(first); + cx.notify(); + }); + assert_eq!(outline(&desk, cx), [(1, "A".into(), 0)]); +} + +#[gpui::test] +fn a_folder_lists_its_markdown_files(cx: &mut TestAppContext) { + let (data, files) = ( + tempfile::tempdir().expect("data"), + tempfile::tempdir().expect("files"), + ); + std::fs::create_dir(files.path().join("sub")).expect("sub"); + std::fs::write(files.path().join("a.md"), "a").expect("a"); + std::fs::write(files.path().join("sub/b.markdown"), "b").expect("b"); + std::fs::write(files.path().join("c.txt"), "c").expect("c"); + let (desk, cx) = start(cx, data.path()); + open(&desk, files.path(), cx); + let mut listed = desk.read_with(cx, |desk, _| { + let folder = desk.folder.as_ref().expect("a folder"); + folder + .files + .iter() + .map(|file| file.replace('\\', "/")) + .collect::>() + }); + listed.sort(); + assert_eq!(listed, ["a.md", "sub/b.markdown"]); +} diff --git a/apps/ely-mark/src/side.rs b/apps/ely-mark/src/side.rs index e1e6922..6e8950f 100644 --- a/apps/ely-mark/src/side.rs +++ b/apps/ely-mark/src/side.rs @@ -253,7 +253,7 @@ impl Side { let Some(heading) = key.parse::().ok().and_then(|at| headings.get(at)) else { return log::error!("mark: the outline chose no heading"); }; - // Keys go to the document, at the heading. + // The heading in view, the caret on it. let line = heading.line; view.update(cx, |view, cx| view.reveal(line, window, cx)); }) diff --git a/crates/ely-ui/src/i18n.rs b/crates/ely-ui/src/i18n.rs index c1ca35d..abb2a4d 100644 --- a/crates/ely-ui/src/i18n.rs +++ b/crates/ely-ui/src/i18n.rs @@ -19,7 +19,6 @@ mod tools; mod tree; mod tree_ops; mod tree_places; -mod web; use gpui::{App, SharedString}; const EN: &[(&str, &str)] = &[ @@ -179,7 +178,6 @@ fn catalogs() -> (Catalog, Catalog) { .chain(tree::EN) .chain(tree_ops::EN) .chain(tree_places::EN) - .chain(web::EN) .chain(mark::EN) .chain(settings::EN) .copied() @@ -203,7 +201,6 @@ fn catalogs() -> (Catalog, Catalog) { .chain(tree::ZH_CN) .chain(tree_ops::ZH_CN) .chain(tree_places::ZH_CN) - .chain(web::ZH_CN) .chain(mark::ZH_CN) .chain(settings::ZH_CN) .copied() diff --git a/crates/ely-ui/src/i18n/web.rs b/crates/ely-ui/src/i18n/web.rs deleted file mode 100644 index 3daedd0..0000000 --- a/crates/ely-ui/src/i18n/web.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! Words for bundled web pages. - -pub(super) const EN: &[(&str, &str)] = &[ - ("web.failed", "The page could not show"), - ("web.reload", "Reload"), -]; - -pub(super) const ZH_CN: &[(&str, &str)] = &[ - ("web.failed", "页面无法显示"), - ("web.reload", "重新载入"), -]; diff --git a/crates/ely-ui/src/lib.rs b/crates/ely-ui/src/lib.rs index 89f0b5f..268e590 100644 --- a/crates/ely-ui/src/lib.rs +++ b/crates/ely-ui/src/lib.rs @@ -10,6 +10,8 @@ pub mod updates; pub mod watching; pub use opening::{launch_files, on_open}; +#[cfg(any(test, feature = "test-support"))] +pub use opening::install_no_launch_files; pub use run::run; pub use shell::{Root, Shell, blank, document_root, root, set_document_menus, tool_root}; pub use tasks::{LongTask, TaskState}; diff --git a/crates/ely-ui/src/opening.rs b/crates/ely-ui/src/opening.rs index a67908d..0548264 100644 --- a/crates/ely-ui/src/opening.rs +++ b/crates/ely-ui/src/opening.rs @@ -39,6 +39,12 @@ pub(crate) fn keep(asked: mpsc::UnboundedReceiver, cx: &mut App) { cx.set_global(Asked(Some(asked))); } +/// No files asked at launch, for tests. +#[cfg(any(test, feature = "test-support"))] +pub fn install_no_launch_files(cx: &mut App) { + keep(mpsc::unbounded().1, cx); +} + /// Files asked before the app started: arguments and Finder's. pub fn launch_files(cx: &mut App) -> Vec { let asked = cx.global_mut::().0.as_mut();