From d0b5aede9501bb5b7b0f17f9f9ea9e48ed1bfd05 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 18:46:37 -0400 Subject: [PATCH] R3.1: real paths without \?\ on Windows (dunce); old recent lists read plain Co-Authored-By: Claude Opus 5.5 (1M context) --- Cargo.lock | 5 +++++ agents.md | 1 + apps/ely-mark/Cargo.toml | 1 + apps/ely-mark/src/desk.rs | 2 +- apps/ely-mark/src/desk/tests.rs | 3 ++- apps/ely-mark/src/places.rs | 12 ++++++------ apps/ely-mark/src/restore.rs | 4 ++-- apps/ely-mark/src/side.rs | 2 +- apps/ely-pdf/Cargo.toml | 1 + apps/ely-pdf/src/tests/sign.rs | 2 +- crates/ely-files/Cargo.toml | 1 + crates/ely-files/src/recent.rs | 24 ++++++++++++++++++++---- crates/ely-files/src/save.rs | 4 ++-- crates/ely-files/src/session/registry.rs | 2 +- crates/ely-lok/Cargo.toml | 1 + crates/ely-lok/src/ffi.rs | 2 +- crates/ely-lok/src/handshake.rs | 2 +- crates/ely-lok/src/worker.rs | 2 +- crates/ely-office/Cargo.toml | 1 + crates/ely-office/src/save.rs | 4 ++-- tasks/TASKS.md | 7 +++++++ 21 files changed, 59 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdf52e2..e8c40af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1909,6 +1909,7 @@ name = "ely-files" version = "0.1.3" dependencies = [ "aes-gcm", + "dunce", "ely-platform", "getrandom 0.4.3", "keyring", @@ -1977,6 +1978,7 @@ name = "ely-lok" version = "0.1.3" dependencies = [ "base64 0.23.1", + "dunce", "ely-gpui-component", "ely-ipc", "ely-platform", @@ -2007,6 +2009,7 @@ version = "0.1.3" dependencies = [ "anyhow", "base64 0.23.1", + "dunce", "ely-files", "ely-gpui-component", "ely-office", @@ -2031,6 +2034,7 @@ version = "0.1.3" dependencies = [ "anyhow", "directories", + "dunce", "ely-files", "ely-gpui-component", "ely-lok", @@ -2049,6 +2053,7 @@ dependencies = [ name = "ely-pdf" version = "0.1.3" dependencies = [ + "dunce", "ely-files", "ely-gpui-component", "ely-office", diff --git a/agents.md b/agents.md index bdb9516..fa202de 100644 --- a/agents.md +++ b/agents.md @@ -284,3 +284,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 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 核对两处加载都结束、错误里没有所在文件夹名。C14.8:左栏三页照旧(文件夹树、大纲、搜索);大纲在纵向滚动区里(标题再多也选得到,侧栏测试以 100 个标题滚到最后),取组件按 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`)删去。C14.9(组件库 ce4a1e9):回车照列表续写(无序、有序加一、任务为未勾),空项回车即结束列表,源码与打开的块都如此,只在 markdown-rs 读作列表项且不在代码或公式里时(围栏里的 `- a` 照常换行;长文读完前源码里回车只换行),新项照所在项的引用前缀、缩进(空格或 Tab)、标记后的空白与任务框写出,父项标记处换成空格(组件库 e162082);没写正文的任务框也算空项,回车即结束列表;Shift+Tab 按列退回一级:行首的空格与 Tab 混写时,Tab 补到下一个四格位,去掉合计四列(组件库 b39097c);在空白行(含只有空格的行)插入表格即以表格取代该行(上一行有字时先空一行),免成缩进代码(组件库 cd16b20);打开的块里 Tab、Shift+Tab 把所触的行缩进或退回四格(`MarkdownDocument > ElyInput` 键位,块外 Tab 照旧在块间移动),源码由编辑器自己缩进;`MarkdownDocument::edit_table` 按插入点所在表格增删行列,`insert_table` 在插入点所在行后放一个三列表格,都为一步撤销,表格按规范写法重写(引用与列表里的表格各行照留其前缀,前缀不一即拒绝;表头之外的多余单元格照留,在其上的列操作拒绝;拒绝的原因经 `TableRefusal` 交给应用提示),预览里一切修改(表格、替换)都拒绝;`show_match` 在源码选中、可视打开其所在块并选中、预览转到其行,`replace` 一步替换(自己的写入计入,已打开的块不被关掉);文字未读完时 `show_match` 先读,长文等读完在下一帧落定(记下所等的文字版本:其间文字一变即作废,新的请求取代等着的);围栏代码按其语言着色(`forms::syntax_fenced`,散文名不着色,无名照通用着色)。ElyMark:Cmd/Ctrl+F 与“编辑›查找和替换”打开查找框(组件库 `FindWidget`,`find_all` 的大小写、全词、正则;正则写错即框内显示原因),源码里标出全部匹配与当前一处;替换一处后转到替换文字之后的下一处(到尾即回到第一处),全部替换为一步撤销;“表格”菜单插入表格与增删行列,插入点不在表格里即提示。应用测试以整窗核对计数、替换与撤销、正则错误、跳到下一处、表格菜单。C14.10(组件库 d126a7e):拖到文档上的文件与剪贴板里只有图片(没有文字)时的粘贴,组件以 `Attachment` 事件交给属主(源码、打开的块与可视无块时都接 Cmd/Ctrl+V),`MarkdownDocument::insert` 在插入点写入文字(打开的块里照键入,预览拒绝)。ElyMark 的 `attach.rs`:只收图片(png、jpg、jpeg、gif、webp、svg、bmp,别的文件提示不收),在后台复制到文件旁的 `assets/`(拖入的保留原名、重名加序号,粘贴的名为 `pasted-毫秒.扩展名`;`ely_files::import_attachment` 与新的 `write_attachment` 同一套:以 `create_new` 占名,从不覆盖,写坏即删去),完成时文件仍在原文件夹才在插入点写 `![名](assets/名)`(名中的控制字符换成空格,方括号与反斜杠转义;地址除字母数字与 `-._~` 外逐字节百分号转义;文件名不是规范文字的图片在复制前即拒绝并说明,链接从不改写文件名),否则提示图片留在哪里;未保存过的文档与预览拒绝并说明。视图的文件夹只在外壳告知已存文件的位置时才有(`MarkView::moved`),图片的读取位置随之;未命名文档没有文件夹。`Opened` 去掉(只剩源),引擎直接载入 `Source`。C14.11(组件库 e89fb32):`MarkdownDocument::reset` 换文字时保留插入点与顶行(按新文字夹住,顶行等新文字读完后落定),干净的标签接过磁盘文字即停在原处,撤销不回到旧文字;外部变更、改名跟随、删除与读错照旧(`outside.rs`;`ely_files::Fingerprint` 加上文件身份,保留修改时间的原子替换也认作变更,各应用的冲突核对同受其益;每次读盘编号,只认最新一次,落定前再核对磁盘仍是所读的那份,否则重看),比较后的合并经 `view.adopt` 为一步撤销。应用测试以整窗核对:干净重载保位与撤销止于磁盘,删除后留着文字并标为已删,改名后标签跟到新名,路径变成文件夹即标为读不出,合并为一步撤销,保留时间的替换照样重载,过期的读取不落定。C14.12(组件库 508fee9):`MarkdownDocument::set_look(DocumentLook)` 是书写视图,只管怎么显示:`measure` 给正文栏的最大宽并居中(三种模式),`font` 给可视与预览的正文字体(代码照用等宽),`scale` 乘以正文与标题的字号(可视、预览经渲染的 `Draw`,打开的块同样,源码经 `CodeEditor::set_scale`),`focus` 让可视里读者所在块之外的块、源码里插入点所在行之外的行淡到 0.35(`CodeEditor::set_focus_line`),`typewriter` 让可视里读者所在块(打开的或插入点所在的)、源码里插入点所在行居中(块、文字、键入、块高或可视区一变即重新居中;两端各留半屏余量,首末块与首末行也能居中:可视放在第一块之上与末尾项之下,源码为列表首尾的空行(行数取正文区高度的一半,绘制、输入法与点按的坐标同一原点);模式切换的定位落定后重新居中;源码在渲染时以严格居中滚动,`CodeEditor::set_typewriter`;居中记录含视图与行高;组件库 f102ea0、cf9cc19、7ed4ea8)。ElyMark 的 `look.json` 照旧(专注、打字机、窄中宽 36/48/64rem、无衬线/衬线/等宽、字号 70% 至 200%),新标签首次报到即取用,菜单一改即作用于全部标签并存下(存不下即提示);衬线字体为 Georgia(Linux 为 DejaVu Serif,未在 Linux 上核对是否装有);专注模式另收起标签条、左栏与状态栏(照旧)。搜索(C14.13,MARK-10)照旧:本文件或所开文件夹,不分大小写逐行找,打开着的文件按当前文字;每条结果另记该行第一处命中的字符列(查询与原文都逐字去掉大小写,词尾 `ς` 同 `σ`,照 Unicode 的大小写折叠;逐字对应回原文,`İ` 等变长的字照样对得上),选中即打开或转到该文件,插入点落在该处。输出(C14.14,MARK-11):“文件›导出…”(⌘E)一个对话框选 HTML、HTML 与资源文件夹、PDF、纯文本,再选位置;HTML 由 ElyMark 按 markdown-rs 的语法树逐个节点写出(`output/html.rs`:GFM 加数学,front matter 不进输出,文档里的 HTML 一律转义成文字,链接按浏览器的读法(去掉首尾空白与控制字符、删去制表与换行)判定协议,只放行无协议、网页与邮件地址,脚注照 GFM 放在页末),公式换成 RaTeX 的轮廓 SVG(组件库 `formula_svg`,黑色;RaTeX 没有轮廓而写成文字的字,与图一样经 usvg 以系统字体变成路径,找不到字体即报错;先变路径再按墨迹定边框),Mermaid 图换成浅色 SVG,经 usvg 把箭头标记与文字都变成路径(组件库 `diagram_svg`,与公式共用 `flatten`;有标签或字找不到字体即报错,不让字悄悄消失),画不出的公式与图照原代码保留,按各自所在的行提示;图片:自包含的 HTML 内嵌为 data URL,资源文件夹版拷进新建的 `名称_files`(以 `create_new` 占名,由文件系统判同名,大小写不分的盘上也不互相覆盖;文件夹已在即拒绝,写坏只删本次自己建的(以建成与否为准,不先查是否存在)),网址与 `data:image` 原样保留,读不到的在 HTML 里照原地址、在 PDF 里写成方括号里的替代文字,并都提示;纯文本取自 mdast(块间空行,表格以制表符分格,公式写 TeX)。PDF:同一页面与图片写进标签暂存目录下每次各自的文件夹,`ely-pdf-worker` 的 `HTML_PDF` 以 MuPDF 的 story 排成 A4(边距 2 厘米),MuPDF 读不了的图(WebP、网址)事先列为缺图;story 或绘制有任何警告即整份失败、不写文件(同图片导出);暂存随即删去。打印(⌘P)先写同一份 PDF(每次打印各自的文件)再交系统面板(`ely_office::print_later`)。“编辑›复制为富文本”:所选的 Markdown(源码模式里编辑器的选区,或打开的块的选区,组件库 `MarkdownDocument::selected`;可视与预览没打开块时没有选区,即全文)以同一套生成 HTML(图片内嵌)放进剪贴板,纯文本为 Markdown 本身;缺图、画不出的公式与图同导出一样提示。会话恢复(C14.15,MARK-12)照旧(`restore.rs`:`session.json` 记标签的路径、名称、模式、插入点与滚动,`look.json` 记视图设置,崩溃后未保存的文字以未命名副本回来,原文件不动);恢复点或会话写不进时,窗口以提示写明(恢复点另写明该标签上次存下的时间,未曾存下写“尚无”),同一类错误(按 `io::ErrorKind`,不按含临时文件名的原话)只提示一次,每次计时照样重试;崩溃后恢复的标签以所载恢复点的时间为“上次存下”;所记的模式改为类型化的 `Mode`(serde 按小写名),认不出的模式与其他坏字段一样使会话读不出、启动报错,不再悄悄当作可视;原由已删的 CEF 窗口脚本覆盖的重启与崩溃恢复,改由同一进程里两个 gpui 测试上下文先后开同一数据目录核对。Git 配合(C14.16,MARK-13)照旧:状态栏的 Git 项(未改动、有改动、未跟踪、已忽略、有冲突)在保存(指纹变化)后重问,按下即以该文件开 ElyTree;单测核对各状态,窗口测试核对保存后由未改动变为有改动(测试的提交关掉签名,不受用户 `commit.gpgsign` 影响)。组件库 50a59d2 至 58227cd:图在屏上的尺寸与公式的边框改用带系统字体的 usvg 读取,选字照 gpui 的渲染器(字库为系统字体加组件库资源里 gpui 自带的 IBM Plex Sans,没有字体答应的通用族改指自带字体;指名的字体族找不到即取 sans-serif,再找不到取任一字体,单个字再按字回退),导出也照此(原先用无字体的默认选项量尺寸,每张图的每个标签都记一条“找不到字体”的警告,公式的边框也算不进回退字的墨迹)。验收(2026-10-12):Windows 与 Linux(WSL)的 `packaging/stage.sh ely-mark` 带出 `ely-mark`、`ely-pdf-worker`、Git 与许可,冒烟(`ELY_SMOKE=1`,打开含公式与图的文件)均正常退出、Windows 日志无警告;macOS 只能经发布 CI 构建,本次未验证。已知限制:markdown-rs 的解析时间随块数平方增长(release,本机:短段落 1 万段 0.12 秒、2 万 0.40 秒、4 万 1.44 秒、8 万 5.5 秒,纯 CommonMark 同样),所以短段落极多的大文件导出要数秒;PERF-07 的 1 MB 样本仍在 2 秒内。Linux 上 gpui 的 SVG 渲染器找不到其自带的 Lilex 字体(共用资源里没有),每次启动记一条警告,改用系统字体,不属 ElyMark。导出不写打开着的文件(`ely_files::export` 与外壳的持有者检查)。ElyMark 因此依赖 `ely-pdf-engine`,各端的包随带 `ely-pdf-worker` 与 MuPDF、OpenSSL、Great Vibes 的许可;OCR 语言数据只随 ElyPDF。 +- 2026-10-12(用户报:Windows 上 ElyMark 的最近列表显示 `\\?\C:\…`):凡取真实路径一律 `dunce::canonicalize`(ely-files 的会话登记与导出、ely-office 的另存身份、ely-lok 的内核目录与配置目录、ElyMark 的文件夹、最近列表、会话与标签比对;ElyTree 原已如此):Windows 上能写成普通路径即普通路径,他处同 `std::fs::canonicalize`。`ely_files::recent` 读入时以 `dunce::simplified` 去掉旧版写下的 `\\?\` 并去重,下次写入即为普通路径。 diff --git a/apps/ely-mark/Cargo.toml b/apps/ely-mark/Cargo.toml index 94a62b6..040b7bc 100644 --- a/apps/ely-mark/Cargo.toml +++ b/apps/ely-mark/Cargo.toml @@ -11,6 +11,7 @@ build = "../../packaging/windows/icon.rs" embed-resource.workspace = true [dependencies] +dunce.workspace = true anyhow.workspace = true base64.workspace = true ely-files.workspace = true diff --git a/apps/ely-mark/src/desk.rs b/apps/ely-mark/src/desk.rs index 13d0e53..b9d59dd 100644 --- a/apps/ely-mark/src/desk.rs +++ b/apps/ely-mark/src/desk.rs @@ -137,7 +137,7 @@ impl Host for Desk { view.update(cx, |view, cx| view.moved(folder.to_path_buf(), cx)); } self.track_recent(id, &path, cx); - let real = std::fs::canonicalize(&path).ok(); + let real = dunce::canonicalize(&path).ok(); if self .going .as_ref() diff --git a/apps/ely-mark/src/desk/tests.rs b/apps/ely-mark/src/desk/tests.rs index 4ae2ea6..23d3f70 100644 --- a/apps/ely-mark/src/desk/tests.rs +++ b/apps/ely-mark/src/desk/tests.rs @@ -75,10 +75,11 @@ fn a_file_opens_with_its_outline_and_joins_the_recent_list(cx: &mut TestAppConte 4, "the jump lands" ); - let real = std::fs::canonicalize(&path).expect("real"); + let real = dunce::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}"); + assert!(!kept.contains(r"\\\\?\\"), "plain paths only: {kept}"); } #[gpui::test] diff --git a/apps/ely-mark/src/places.rs b/apps/ely-mark/src/places.rs index 14999d3..5245b8a 100644 --- a/apps/ely-mark/src/places.rs +++ b/apps/ely-mark/src/places.rs @@ -28,7 +28,7 @@ impl Desk { let files = cx .background_executor() .spawn(async move { - let real = std::fs::canonicalize(&listed)?; + let real = dunce::canonicalize(&listed)?; Ok::<_, std::io::Error>((markdown_under(&real)?, real)) }) .await; @@ -65,7 +65,7 @@ impl Desk { /// Puts `path` first in the recent list. pub fn remember(&mut self, path: &std::path::Path, cx: &mut Context) { // One spelling per file: its real path. - let remembered = std::fs::canonicalize(path) + let remembered = dunce::canonicalize(path) .and_then(|path| ely_files::recent::remember(&self.recent_file, &path)); match remembered { Ok(list) => self.recent = list, @@ -82,7 +82,7 @@ impl Desk { /// Tab `id` shows `path`; the recent list follows. pub fn track_recent(&mut self, id: u64, path: &std::path::Path, cx: &mut Context) { - let Ok(real) = std::fs::canonicalize(path) else { + let Ok(real) = dunce::canonicalize(path) else { return log::warn!("mark: {} is gone; the recent list keeps it", path.display()); }; if self.remembered.get(&id) == Some(&real) { @@ -129,7 +129,7 @@ impl Desk { return cx.notify(); } // A file gone is reported by the open itself. - if let Ok(real) = std::fs::canonicalize(&file) { + if let Ok(real) = dunce::canonicalize(&file) { self.going = Some((real, at)); } self.open_file(file, cx); @@ -137,11 +137,11 @@ impl Desk { /// The tab showing `path`, by real path. pub fn tab_of(&self, path: &std::path::Path) -> Option { - let real = std::fs::canonicalize(path).ok()?; + let real = dunce::canonicalize(path).ok()?; let held = |tab: &&ely_office::Tab| { tab.session .path() - .and_then(|open| std::fs::canonicalize(open).ok()) + .and_then(|open| dunce::canonicalize(open).ok()) == Some(real.clone()) }; self.core.tabs.iter().find(held).map(|tab| tab.id) diff --git a/apps/ely-mark/src/restore.rs b/apps/ely-mark/src/restore.rs index d12514a..5949968 100644 --- a/apps/ely-mark/src/restore.rs +++ b/apps/ely-mark/src/restore.rs @@ -91,7 +91,7 @@ impl Keeping { Marker::Clean | Marker::Discarded => None, }; if let Some(path) = &kept.path { - match std::fs::canonicalize(path) { + match dunce::canonicalize(path) { // The file reopens as it is on disk. Ok(real) => waiting.push(Waiting { kept: Kept { @@ -215,7 +215,7 @@ impl Desk { let real = tab .session .path() - .and_then(|path| std::fs::canonicalize(path).ok()); + .and_then(|path| dunce::canonicalize(path).ok()); let Some(Waiting { kept, text: back, .. }) = self.keeping.take(real.as_ref(), id) diff --git a/apps/ely-mark/src/side.rs b/apps/ely-mark/src/side.rs index ac709f5..aa87af5 100644 --- a/apps/ely-mark/src/side.rs +++ b/apps/ely-mark/src/side.rs @@ -111,7 +111,7 @@ impl Side { // Open files by real path, as the folder lists them. let reals: Vec<(u64, PathBuf)> = open .iter() - .filter_map(|(id, path, _)| Some((*id, std::fs::canonicalize(path).ok()?))) + .filter_map(|(id, path, _)| Some((*id, dunce::canonicalize(path).ok()?))) .collect(); let sources: Vec<(Option, PathBuf)> = match (self.wide, &desk.folder) { (true, Some(folder)) => folder diff --git a/apps/ely-pdf/Cargo.toml b/apps/ely-pdf/Cargo.toml index 88e9557..9c71fb4 100644 --- a/apps/ely-pdf/Cargo.toml +++ b/apps/ely-pdf/Cargo.toml @@ -22,6 +22,7 @@ image.workspace = true log.workspace = true [dev-dependencies] +dunce.workspace = true ely-ui = { workspace = true, features = ["test-support"] } ely-pdf-engine = { workspace = true, features = ["test-support"] } ely-gpui-component = { workspace = true, features = ["test-support"] } diff --git a/apps/ely-pdf/src/tests/sign.rs b/apps/ely-pdf/src/tests/sign.rs index a8952e1..ea89a75 100644 --- a/apps/ely-pdf/src/tests/sign.rs +++ b/apps/ely-pdf/src/tests/sign.rs @@ -136,7 +136,7 @@ fn save_signed_as(cx: &mut VisualTestContext, viewer: &Entity, old: &Ent let id = cx.update(|_, app| viewer.read(app).core.tabs[0].id); let target = copy.to_path_buf(); std::fs::write(copy, b"").expect("a place"); - let copy = std::fs::canonicalize(copy).expect("the real path"); + let copy = dunce::canonicalize(copy).expect("the real path"); std::fs::remove_file(©).expect("clears the place"); cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, target, None, window, cx))); settle(cx, |app| { diff --git a/crates/ely-files/Cargo.toml b/crates/ely-files/Cargo.toml index c5c37b2..0323284 100644 --- a/crates/ely-files/Cargo.toml +++ b/crates/ely-files/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] +dunce.workspace = true ely-platform.workspace = true log.workspace = true aes-gcm.workspace = true diff --git a/crates/ely-files/src/recent.rs b/crates/ely-files/src/recent.rs index 6c5e965..c8f2640 100644 --- a/crates/ely-files/src/recent.rs +++ b/crates/ely-files/src/recent.rs @@ -10,12 +10,21 @@ const KEEP: usize = 20; /// Recently opened paths, newest first. pub fn recent(file: &Path) -> io::Result> { - match std::fs::read(file) { + let listed: Vec = match std::fs::read(file) { Ok(bytes) => serde_json::from_slice(&bytes) - .map_err(|error| io::Error::other(format!("{}: {error}", file.display()))), - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Vec::new()), - Err(error) => Err(error), + .map_err(|error| io::Error::other(format!("{}: {error}", file.display())))?, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(error), + }; + // Older builds wrote `\\?\` paths. + let mut list = Vec::with_capacity(listed.len()); + for path in listed { + let plain = dunce::simplified(&path).to_path_buf(); + if !list.contains(&plain) { + list.push(plain); + } } + Ok(list) } /// Takes `root` out of the list. @@ -66,6 +75,13 @@ mod tests { assert_eq!(list[..2], [PathBuf::from("/r/10"), PathBuf::from("/r/24")]); forget(&file, Path::new("/r/10")).expect("forgets"); assert_eq!(recent(&file).expect("reads")[0], PathBuf::from("/r/24")); + #[cfg(windows)] + { + let old = [r"\\?\C:\r\old", r"C:\r\old", r"\\?\C:\r\new"]; + std::fs::write(&file, serde_json::to_vec(&old).expect("json")).expect("writes"); + let plain = [PathBuf::from(r"C:\r\old"), PathBuf::from(r"C:\r\new")]; + assert_eq!(recent(&file).expect("reads"), plain); + } std::fs::write(&file, "not json").expect("writes"); assert!( remember(&file, Path::new("/r/1")).is_err(), diff --git a/crates/ely-files/src/save.rs b/crates/ely-files/src/save.rs index 1f576da..a93231c 100644 --- a/crates/ely-files/src/save.rs +++ b/crates/ely-files/src/save.rs @@ -183,7 +183,7 @@ pub fn export( /// The canonical form of a possibly missing path. fn resolved(path: &Path) -> io::Result { - match std::fs::canonicalize(path) { + match dunce::canonicalize(path) { Ok(real) => Ok(real), Err(error) if error.kind() == io::ErrorKind::NotFound => { let name = path.file_name().ok_or_else(|| { @@ -192,7 +192,7 @@ fn resolved(path: &Path) -> io::Result { format!("{} has no file name", path.display()), ) })?; - Ok(std::fs::canonicalize(parent_dir(path))?.join(name)) + Ok(dunce::canonicalize(parent_dir(path))?.join(name)) } Err(error) => Err(error), } diff --git a/crates/ely-files/src/session/registry.rs b/crates/ely-files/src/session/registry.rs index 5d2845d..824047a 100644 --- a/crates/ely-files/src/session/registry.rs +++ b/crates/ely-files/src/session/registry.rs @@ -83,7 +83,7 @@ impl Sessions { /// The key of an existing file; missing fails loud. fn key(path: &Path) -> io::Result { Ok(Key { - canonical: std::fs::canonicalize(path)?, + canonical: dunce::canonicalize(path)?, id: file_id(path)?, }) } diff --git a/crates/ely-lok/Cargo.toml b/crates/ely-lok/Cargo.toml index 7f0497c..a9a520f 100644 --- a/crates/ely-lok/Cargo.toml +++ b/crates/ely-lok/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] +dunce.workspace = true ely-ipc.workspace = true base64.workspace = true serde_json.workspace = true diff --git a/crates/ely-lok/src/ffi.rs b/crates/ely-lok/src/ffi.rs index 781c2a3..8191cfc 100644 --- a/crates/ely-lok/src/ffi.rs +++ b/crates/ely-lok/src/ffi.rs @@ -74,7 +74,7 @@ impl Lok { /// dlopens the LOK library and hooks with the profile URL. pub fn hook(install: &Path, profile_url: &str) -> io::Result { - let install = std::fs::canonicalize(install)?; + let install = dunce::canonicalize(install)?; let install = install.as_path(); let install_str = install.to_string_lossy().into_owned(); let install_c = cstring(&install_str)?; diff --git a/crates/ely-lok/src/handshake.rs b/crates/ely-lok/src/handshake.rs index 6ad10e8..514bd10 100644 --- a/crates/ely-lok/src/handshake.rs +++ b/crates/ely-lok/src/handshake.rs @@ -53,7 +53,7 @@ impl Worker { pub(crate) fn tile_self_check(&mut self, profile_dir: &Path) -> io::Result { let doc = profile_dir.join("lok-verify.fodt"); std::fs::write(&doc, VERIFY_DOC)?; - let url = ipc::file_url_of(&doc.canonicalize()?); + let url = ipc::file_url_of(&dunce::canonicalize(&doc)?); let payload = self.request(tag::VERIFY, &Pack::new().str(&url).bytes())?; let red = Unpack::new(&payload).u64()?; Ok(red > 0) diff --git a/crates/ely-lok/src/worker.rs b/crates/ely-lok/src/worker.rs index 999ee37..ea5446b 100644 --- a/crates/ely-lok/src/worker.rs +++ b/crates/ely-lok/src/worker.rs @@ -36,7 +36,7 @@ pub fn run( output: impl Write + Send + 'static, ) -> io::Result<()> { set_output(Box::new(output)); - let profile_dir = std::fs::canonicalize(profile_dir)?; + let profile_dir = dunce::canonicalize(profile_dir)?; let profile_url = profile_url_of(&profile_dir); let _ = teardown::PROFILE.set(profile_dir.clone()); let lok = Lok::hook(Path::new(install), &profile_url)?; diff --git a/crates/ely-office/Cargo.toml b/crates/ely-office/Cargo.toml index 0089804..21bc406 100644 --- a/crates/ely-office/Cargo.toml +++ b/crates/ely-office/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] +dunce.workspace = true anyhow.workspace = true directories.workspace = true ely-files.workspace = true diff --git a/crates/ely-office/src/save.rs b/crates/ely-office/src/save.rs index 41ee466..9a7c5d5 100644 --- a/crates/ely-office/src/save.rs +++ b/crates/ely-office/src/save.rs @@ -14,14 +14,14 @@ use crate::{After, Closing, Dialog, Engine, Host, Opening, Tab, Tabs, in_window, /// The file a path names, links resolved. fn identity(path: &Path) -> io::Result { - match std::fs::canonicalize(path) { + match dunce::canonicalize(path) { Ok(real) => Ok(real), Err(error) if error.kind() == io::ErrorKind::NotFound => { let parent = path.parent().unwrap_or(Path::new(".")); let name = path .file_name() .ok_or_else(|| io::Error::other("no file name"))?; - Ok(std::fs::canonicalize(parent)?.join(name)) + Ok(dunce::canonicalize(parent)?.join(name)) } Err(error) => Err(error), } diff --git a/tasks/TASKS.md b/tasks/TASKS.md index f80fa5d..5761374 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -221,3 +221,10 @@ - [x] L1.6 本机出六个应用的四种包;Omarchy 上装 Arch 包与 AppImage 出窗口;deb、rpm 在各自发行版的容器里装上并冒烟;`linux-installed.sh`、`linux-distros.sh`、`linux-mark-idle.sh` — R1 deb/rpm 未运行、外部改动未核对显示;R2 内核未出图也过、OCR 大小写、清理竞态;R3 公式栏冒充网格、OCR 与 pgrep 错误被吞;R4 取色、存活、截图错误被吞;R5 hyprctl 查询失败被当作未出窗口(第五轮后修正并以注入失败核对) - [x] L1.7 v0.1.3:删去旧 AppImage,上传新包;`ci.sh` 的 Linux 产出四种包;agents.md 与官网随之(官网只链到发布页,无需改);`package.sh` 拒绝指向包外的 RUNPATH;Linux CI 进容器;`packaging/source.sh` 随发布上传本提交的源码 — R1 前缀检查可越出、读错被吞、CI 未进容器、无对应源码;R2 空项、拆分错误、未跟踪文件;R3 换行、git 配置;R4 `]` 加换行;R5 末尾换行(第五轮后修正并以编译的 ELF 核对) - [x] L1.8 macOS 发布:CI 从零编 LibreOffice 超过 act_runner 的 3 小时被杀(v0.1.3 无 dmg);Mac 任务以 `ELY_SHARE` 借用 Mac mini 开发克隆 `ElyOfficeSuite` 的引擎,CI 克隆里自建的残留引擎即报错而非混用;`tests/scripts/ci-share.sh` — R1 链接目标末尾换行;R2 测试只看文字;R3 目标仍截换行、reference 未测;R4 未按 macOS 跑;R5 PASS + +## R3 0.1.4(用户 2026-10-12 报:Windows 与 Linux 上的路径、缩放、表格指针;修完提交、推送,版本 0.1.4,不发布) + +- [ ] R3.1 路径不带 `\?\`:凡取真实路径一律 `dunce::canonicalize`(Windows 上能写成普通路径即普通路径,他处同 `std::fs::canonicalize`);最近列表读入时去掉旧的 `\?\` 并去重;ElyMark 最近列表、ElyTree 等界面不再显示 `\?\` +- [ ] R3.2 缩放:Doc、Table、Show、PDF、Mark 以 Ctrl 加滚轮(macOS 为 Cmd)缩放,原认 Windows 键与 Super(Hyprland 占用,Linux 上无效);滚轮一格为一档(原 Windows 一格三行,一下放大 1.8 倍),触控板的像素滚动照旧连续(LOK)或累积成档(PDF、Mark);PDF 与 Mark 原无滚轮缩放 +- [ ] R3.3 LOK 视图的指针随内核:`LOK_CALLBACK_MOUSE_POINTER`(单元格上为箭头,编辑单元格或文字时为文字指针,边线为拖动指针,链接为手),原一律为文字指针 +- [ ] R3.4 版本 0.1.4:`Cargo.toml` 与随版本的文档,提交并推送 main(不打标签、不发布)