Repository navigation
v0.4.0 — 可点选目录 / 排序与时间修正 / 分类改名删除 / CRLF 笔记与表格渲染修复 / 索引改相对路径
v0.4.0 — 可点选目录 / 排序与时间修正 / 分类可改名可删除 / CRLF 笔记打不开 / 索引改记相对路径(2026-10-01)
修
- 新建分类立刻报「分类不存在」 —— 行身份取错了字段:
TreePane里行内改名用的是
row.id,而Row上从来没有这个字段(取出来是undefined),于是
renameCollection(ws, undefined, name)返回 false,Host 抛「分类不存在」。
新建分类走的是"先建默认名、再立刻进 行内改名",所以看起来是一建就报错。
改成从行 key 上取 id(c:<id>/n:<id>,笔记那条分支本来就是这么写的)。
旁证:用户树里那 5 个还叫「新分类」的分类,就是这条的症状。 - 「最近」其实不是按时间排 —— Host 用
Number(entry.version)当 mtime,而真 provider
(@deepseek-ai/dsh-fs-local)的 version 是`${dev}:${ino}:${size}:${mtimeNs}:${ctimeNs}`,
Number(...)是 NaN →at恒为 0 → 全部并列 → 实际按路径排。现在按严格形状解析第 4 段
(mtimeNs),认不出的形状一律 0(回落路径序,绝不瞎猜)。测试替身一直给
String(mtimeMs),所以这条在单测里永远测不出来 —— 新测试改用真形状,与运行时同构。 - 打开 CRLF 笔记报
Selection points outside of document—— CM6 的
EditorState.create({ doc })用/\r\n?|\n/切分重建,CRLF 文件的doc.length
比磁盘原文少掉「CR 的个数」;initialAnchor拿原文长度当坐标,而那一处
dispatch({selection:{anchor}})又是全项目唯一没夹取的。用户那篇 123KB / 1244 个 CR
的笔记正中:状态栏以前显示 75404,现在是 74160(= 75404 − 1244)。 - 保存会静默把 CRLF 改成 LF ——
fs.writeText是原样写(不像官方editText会
restoreLineEndings)。现在保存前采样文件头判断行尾并按原风格写回;
FS_STALE_VERSION交出去的currentText也归一成 LF —— 否则 CRLF 笔记上
"磁盘内容 == 我正要写的内容"那条自愈分支永远不成立,会误报外部修改。 - 超过 64KB 的笔记根本存不下去 —— 路由的 JSON 请求体上限是 64KB,而
save走的正是它:
用户那篇 123,530 字节的笔记每次保存都被挡住,界面只显示「Save failed」,
真正的原因(请求体过大)藏在响应里。上限提到 8MB(本地同源路由、自带鉴权,
笔记是纯文本;资产那条本来就是 16MB)。靶子用例:一条 >128KB 的 body 必须 200。 - 表格里的 markdown 完全没渲染 —— 单元格以前是
td.textContent = cell.text,
于是**加粗**、$公式$、==高亮==、`代码`、[[双链]]在表格里全是原文。
现在每格单独跑决策层再落成 DOM(见editor/cell-inline.ts/cell-render.ts)——
规则不在第二处重写,"该藏还是该露"仍然只有决策层说了算。公式走 Temml(与正文同一个渲染器)。 - 表格列一多就把整篇顶宽(分屏时正文也得左右拖) —— 两个原因是叠加的:
① 单元格里的长文本不换行,把列撑到几千像素;
②.cm-content是 CM6 的 flex item(flex-grow:2; flex-shrink:0),
它的min-width:auto等于min-content 宽度 —— 宽表格的 min-content 把内容区撑到比滚动区还宽
(实测:滚动区 384px、内容区 687px),于是连正文都在 687px 上换行。
修法:表格自己滚(外壳overflow-x:auto; max-width:100%)+
内容区min-width:0(让正文老老实实等于滚动区宽度)+ 单元格换行。
现在照 Typora:正文一律自适应、没有横向滚动条;只有表格在列多时出横向滚动条。 - 渲染出来的图片不跟着横向宽度自适应 ——
inline-flex外壳的宽度是由图片本身决定的,
子元素再写max-width:100%只是"相对自己"的空转,一张 2000px 宽的图照样把正文顶宽。
现在外壳也带max-width:100%(百分比相对行/内容区解析,这才是真约束),
图片max-width:100% + height:auto(只缩不放、保持长宽比)。
新增
- 「选择目录…」:扫描范围不再只能手打。 面板内浏览器(面包屑 + 子目录列表,
种子 = 工作区根,且不许走出工作区;点目录/SKIP_DIRS不给,免得选了也是空的)。
数据来自官方uiWorkspace.listDirectory;拿不到该服务时按钮不出现(官方口径:
藏起入口而不是失败)。特意没用pickDirectory()—— 它要nativecapability,
而本 profile 组合的是browse后端,调用会被directory-picker/unavailable拒绝(实测)。 - 排序:候选/杂项两段支持 时间 / 名称 / 路径 / 大小 × 升降序;「最近」默认时间倒序;
每行显示修改时间(认不出时间的文件不显示那列 —— 不拿假时间骗人)。 - 分类右键:重命名 + 删除。删除只动树(索引里的归属),磁盘上一个文件都不碰:
「笔记移到上级分类」或「笔记留为未归类」。二级菜单本身就是二次动作,不再弹窗确认。 - 扫描范围接受绝对路径:Host 侧换算成工作区相对,并拒绝工作区外的目录
(否则"多看一个目录"就变成越界读用户主目录)。
存储:索引改记相对路径
<工作区>/.dsh-notes/index.json 里,笔记与笔记根从绝对路径改成工作区相对
(notes[].rel / workspaces[].notesRootRel);只有 workspace.root 保持绝对 —— 它是锚点,
也是工作区键的来源。
- 为什么:这个文件就躺在工作区里,它记的每一篇笔记必然在工作区内,记绝对路径等于把
"这台机器的盘符和目录名"抄进用户数据。v0.3.0 的搬迁能力其实是靠装载时那一趟
rebasePath硬扳回来的;改成相对之后搬迁 = 零操作(同一个文件在新根下直接就是对的),
文件也不再泄漏机器路径。 - 迁移:旧文件(绝对
path)装载时自动补出rel,下次落盘即迁移完;键与root的认领逻辑不变。 - 例外:
storeScope: 'home'(机器本地一份整体索引,里面装着多个工作区与跨工作区映射)
仍然用绝对路径 —— 那里只有绝对路径说得清"是哪个文件"。 - 一处刻意的区分:
notesRootOverrideRel用'.'表示"显式设成工作区根",因为''在
这个字段上的含义是"没设过";不区分的话往返一趟就丢(用户显式选过的笔记根会变回默认)。
工程
- 单测 184 → 238 条;
node scripts/build-graph.mjscycles 0。 - 新增
src/client/scan-sort.ts(排序纯逻辑)、src/client/browse-path.ts(目录浏览器纯逻辑)、
src/client/DirPicker.tsx;src/client/editor/cell-inline.ts(单元格行内节点模型,纯函数)、
cell-render.ts(落成 DOM);lib/notes.js新增mtimeOfVersion/detectEol/applyEol/
normalizeEol/relativeToWorkspace;lib/store.js新增relativizeState。 - Host 半热重载打通(本轮实测,profile 侧配置):给
hmr行的root加上本插件的
lib/目录,改 Host 代码约 1 秒后自动热替换插件条目,不用重启;客户端半一直走
dsh-client-hmr的 500ms bundle 轮询,改完npm run build已打开的页面自动换新(不用刷)。
两个坑都写进了 profile 注释:root只能给很小的子目录。给插件根会让 chokidar(默认无限递归 + 跟随软链)把整个工作区
递归进去 —— 实测 6530 个 inotify watch 且持续增长、永不 ready,把 WSL 的 drvfs/9p 打满,
表现是端口在听但永不应答(systemd 还显示 active)。只给lib/是 13 个 watch。- WSL 的 inotify 看不见
/mnt/d(drvfs):独立进程实测,改 ext4(/tmp、/home)能收到
change,改/mnt/d/...收不到任何事件。所以必须开usePolling
(chokidar 选项能经 config 透传:schemastery会保留未声明的键,不用给
@deepseek-ai/dsh-hmr打补丁);interval: 1000,实测代价 +0.5% 单核。