R3.1: real paths without \?\ on Windows (dunce); old recent lists read plain

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 18:46:37 -04:00
1 parent 1066b41cb0
commit d0b5aede95
21 files changed
+59 -24

No files matched your search

Generated
+5
View File
@@ -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",
+1
View File
@@ -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 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印Line truncated
- 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` 去掉旧版写下的 `\\?\` 并去重,下次写入即为普通路径。
+1
View File
@@ -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
+1 -1
View File
@@ -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()
+2 -1
View File
@@ -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]
+6 -6
View File
@@ -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<Self>) {
// 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<Self>) {
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<u64> {
let real = std::fs::canonicalize(path).ok()?;
let real = dunce::canonicalize(path).ok()?;
let held = |tab: &&ely_office::Tab<crate::outside::Extra, crate::engine::Mark>| {
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)
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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<u64>, PathBuf)> = match (self.wide, &desk.folder) {
(true, Some(folder)) => folder
+1
View File
@@ -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"] }
+1 -1
View File
@@ -136,7 +136,7 @@ fn save_signed_as(cx: &mut VisualTestContext, viewer: &Entity<Viewer>, 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(&copy).expect("clears the place");
cx.update(|window, app| viewer.update(app, |viewer, cx| viewer.write_as(id, target, None, window, cx)));
settle(cx, |app| {
+1
View File
@@ -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
+20 -4
View File
@@ -10,12 +10,21 @@ const KEEP: usize = 20;
/// Recently opened paths, newest first.
pub fn recent(file: &Path) -> io::Result<Vec<PathBuf>> {
match std::fs::read(file) {
let listed: Vec<PathBuf> = 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(),
+2 -2
View File
@@ -183,7 +183,7 @@ pub fn export(
/// The canonical form of a possibly missing path.
fn resolved(path: &Path) -> io::Result<PathBuf> {
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<PathBuf> {
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),
}
+1 -1
View File
@@ -83,7 +83,7 @@ impl Sessions {
/// The key of an existing file; missing fails loud.
fn key(path: &Path) -> io::Result<Key> {
Ok(Key {
canonical: std::fs::canonicalize(path)?,
canonical: dunce::canonicalize(path)?,
id: file_id(path)?,
})
}
+1
View File
@@ -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
+1 -1
View File
@@ -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<Self> {
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)?;
+1 -1
View File
@@ -53,7 +53,7 @@ impl Worker {
pub(crate) fn tile_self_check(&mut self, profile_dir: &Path) -> io::Result<bool> {
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)
+1 -1
View File
@@ -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)?;
+1
View File
@@ -7,6 +7,7 @@ license.workspace = true
publish.workspace = true
[dependencies]
dunce.workspace = true
anyhow.workspace = true
directories.workspace = true
ely-files.workspace = true
+2 -2
View File
@@ -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<PathBuf> {
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),
}
+7
View File
@@ -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(不打标签、不发布)