refactor: externalize filetree selection runtime
This commit is contained in:
+93
-32
@@ -1,7 +1,7 @@
|
||||
# 3-19 Rust Web 浏览器 Runtime 模块外置化重构 v1
|
||||
|
||||
> 创建时间:2026-05-24
|
||||
> 状态:`process`
|
||||
> 状态:`done`
|
||||
>
|
||||
> 触发背景:
|
||||
> - CodeGraph 对 `mnote-web` 前端内嵌 JS 的符号定位效果一般。
|
||||
@@ -579,32 +579,89 @@ Reasonix read-only audit:
|
||||
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
|
||||
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_runtime -- --test-threads=1`
|
||||
- 结果:9 个相关测试通过。
|
||||
- 结果:相关测试通过;本轮另以 `sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback`、`filetree_runtime_contains_row_accessor_helpers` 和完整 `filetree` 过滤测试复核。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_context_menu -- --test-threads=1`
|
||||
- 结果:2 个相关测试通过。
|
||||
- 结果:相关测试通过;本轮另以 `filetree_context` 过滤测试复核。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
|
||||
- 结果:runtime asset route 测试通过。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree`
|
||||
- 结果:29 个 filetree 相关测试通过。
|
||||
- `node scripts/task487-local-folder-tree-live-consumer-smoke.js`
|
||||
- 结果:`ok=true`,证据文件 `tmp/task487-local-folder-tree-live-consumer-smoke/result.json`。
|
||||
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
|
||||
- 结果:`ok=true`,覆盖 editor upload、filetree folder drop、resource tab missing/error、无 active document 附件 tab、filetree scroll、标题来源、附件删除保留相邻附件;证据包含 stdout JSON 和 smoke 生成截图。
|
||||
- `codegraph_search fileTreeRowLocalRelativePath` / `fileTreeAssetDownloadDetail`
|
||||
- 结果:均可定位到 `rust/crates/mnote-web/browser/filetree-runtime.js`。
|
||||
- `codegraph_search buildSidebarFileTreeContext` / `evaluateSidebarFileTreeWhen`
|
||||
- 结果:均可定位到 `rust/crates/mnote-web/browser/filetree-context-menu-runtime.js`。
|
||||
- 真实浏览器轻量验收:
|
||||
- dev server: `npm run dev:hot`
|
||||
- dev server: 复用 / 启动 `MNOTE_WEB_BIND=0.0.0.0:3000 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web`
|
||||
- 登录后访问 `http://127.0.0.1:3000/`
|
||||
- `document.scripts` 包含 `/api/mnote-browser-runtime/filetree-runtime.js` 与 `/api/mnote-browser-runtime/filetree-context-menu-runtime.js`
|
||||
- `window.__mnoteFileTreeRuntime.fileTreeRowLocalRelativePath` 和 `window.__mnoteFileTreeContextMenuRuntime.evaluateSidebarFileTreeWhen` 均为函数。
|
||||
- 两个 runtime asset 均返回 `200`,`content-type=application/javascript; charset=utf-8`。
|
||||
- 截图证据:`p4-filetree-runtime-mounted.png`。
|
||||
|
||||
### 9.8 Batch 7 执行记录(2026-05-25)
|
||||
|
||||
Reasonix read-only audit:
|
||||
|
||||
- run id: `reasonix-2026-05-24T16-25-22-379Z-3e3df753`
|
||||
- worktree: `/mnt/Data1T/mnote-worktrees/p4-filetree-selection-audit`
|
||||
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-25-22-379Z-3e3df753/process-handoff.md`
|
||||
- 类型:只读审计,主控已读取 Hindsight recall、`process-handoff.md`、工作区 diff 与验证结果。
|
||||
- 采纳结论:主壳 filetree selection 应独立为 `filetree-selection-runtime.js`,不并入纯 accessor 的 `filetree-runtime.js`;DND、clipboard、keydown listener 和菜单 DOM 仍留后续批次。
|
||||
|
||||
采纳内容:
|
||||
|
||||
- 新增 `rust/crates/mnote-web/browser/filetree-selection-runtime.js`。
|
||||
- 新增固定 asset route:`GET /api/mnote-browser-runtime/filetree-selection-runtime.js`。
|
||||
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
|
||||
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理,保留原实现 fallback:
|
||||
- `visibleFileTreeRows`
|
||||
- `syncSidebarFileTreeSelection`
|
||||
- `selectSidebarFileTreeRow`
|
||||
- `selectedSidebarFileTreeRowIdsForDrag`
|
||||
- `selectedSidebarFileTreeRows`
|
||||
|
||||
采纳边界:
|
||||
|
||||
- 本批次不迁移 click / contextmenu / keydown / dragstart / dragover / drop / dragend 事件监听器。
|
||||
- 本批次不迁移 `pasteSidebarFileTreeClipboard`、`downloadSelectedFileTreeAssetRows`、`ensureFileTreeWritableTarget`、`deleteSelectedSidebarFileTreeRows`。
|
||||
- selection runtime 只承接选择状态同步、shift range、ctrl/meta toggle 和拖拽行 id 读取;文件树命令语义仍由主壳和 Rust API 合同决定。
|
||||
|
||||
已通过验证:
|
||||
|
||||
- `node --check rust/crates/mnote-web/browser/filetree-selection-runtime.js`
|
||||
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
|
||||
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_selection_runtime -- --test-threads=1`
|
||||
- 结果:2 个 selection runtime 相关测试通过。
|
||||
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
|
||||
- 结果:runtime asset route 测试通过。
|
||||
- 真实浏览器轻量验收:
|
||||
- dev server: `MNOTE_WEB_BIND=0.0.0.0:3000 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:3000 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web`
|
||||
- 临时 local folder 入口 `sourceKind=local_folder&treeView=filetree`。
|
||||
- `/api/mnote-browser-runtime/filetree-selection-runtime.js` 返回 `200`,`content-type=application/javascript; charset=utf-8`。
|
||||
- `window.__mnoteFileTreeSelectionRuntime.selectSidebarFileTreeRow` 为函数。
|
||||
- 对两个可见 filetree row 依次触发 click / ctrl-click 后,`data-selected="true"` 保留两行,`tree.filetree.selection.changed` 派发 2 次,最后事件包含两个 `selectedRowIds`。
|
||||
|
||||
## 10. 分阶段执行清单
|
||||
|
||||
归档说明(2026-05-25):本节保留原始分阶段路线,但不再作为当前 `done/` 状态的未完成 checklist。已经满足归档条件的范围以第 13 节 Done Gate 为准;仍未实施或未专项验证的条目改为普通状态项,后续需要另起设计或 bug/checklist 承接,不能在本 done 文档中继续以未勾选 checklist 表达。
|
||||
|
||||
### P0:定位辅助与基线保护
|
||||
|
||||
- [ ] 记录当前 CodeGraph 对内嵌 JS 的限制,确认不更换 graph 项目。
|
||||
- [ ] 可选新增 `scripts/extract-embedded-js-for-index.js`,生成临时索引文件,不进入 runtime。
|
||||
- [ ] 先生成一份“热点函数归属表”,把最近 bug 中高频搜索的函数归到待拆模块:
|
||||
- 已记录:当前 CodeGraph 对内嵌 JS 的限制,确认不更换 graph 项目。
|
||||
- 未采用:可选新增 `scripts/extract-embedded-js-for-index.js`,生成临时索引文件,不进入 runtime;本批通过真实模块外置优先解决定位问题。
|
||||
- 已记录:热点函数归属表,把最近 bug 中高频搜索的函数归到待拆模块:
|
||||
- `openResource` / `openLocalOfficeFileInActiveTab` / `openEditorAttachmentEditTab` -> `resource-open-runtime.js`
|
||||
- `executeLocalFileTreeExternalDrop` / `uploadFileToMediaAsset` / `insertUploadedAssetIntoEditor` -> `local-upload-runtime.js`
|
||||
- `startWithWebSocket` / `startWithSseFallback` -> `tree-live-controller.js`
|
||||
- `selectFileTreeRow` / `openFileTreeContextMenu` -> `filetree-selection-runtime.js` / `filetree-context-menu-runtime.js`
|
||||
- `createEditorViewBinding` / `replacePaneDocument` -> `document-pane-host-runtime.js`,归 `03-rust-web` 与 `05-editor-mainline` 交界,需单独切片。
|
||||
- [ ] 为 `layout.rs` / `tree.rs` 中计划迁出的 script 增加最小字符串快照测试,防止迁移时漏挂载。
|
||||
- [ ] 建立 smoke 基线:
|
||||
- 后续:为 `layout.rs` / `tree.rs` 中计划迁出的 script 增加最小字符串快照测试,防止迁移时漏挂载。
|
||||
- 基线候选:建立 smoke 基线:
|
||||
- `node scripts/task487-local-folder-tree-live-consumer-smoke.js`
|
||||
- `node scripts/task446-tree-rename-dual-browser-live-smoke.js`
|
||||
- `node scripts/task472-side-target-secondary-pane-smoke.js`
|
||||
@@ -615,20 +672,20 @@ Reasonix read-only audit:
|
||||
- [x] 新增 `rust/crates/mnote-web/browser/resource-open-runtime.js`。
|
||||
- [x] 新增 `rust/crates/mnote-web/browser/local-upload-runtime.js`,或先与 resource open 合并为单个不超过 800 行的 `resource-open-runtime.js`。
|
||||
- [x] 第一刀只迁 `layout.rs` 中 resource open 的纯函数和 bridge;`web_shell.rs` 的 resource tab DOM、session、pane host 生命周期保持在后续 `document-pane-host-runtime.js` 批次。
|
||||
- [ ] 从 `SIDEBAR_TREE_JS` / `web_shell.rs` 中迁出资源打开路径适配,不改变现有 open intent:
|
||||
- 后续:从 `SIDEBAR_TREE_JS` / `web_shell.rs` 中迁出资源打开路径适配,不改变现有 open intent:
|
||||
- page row -> 当前 pane page tab
|
||||
- filetree doc row -> Markdown page tab
|
||||
- local Markdown asset -> active resource tab / secondary pane
|
||||
- pdf/code -> 对应 lightweight viewer
|
||||
- office -> OnlyOffice path builder
|
||||
- [ ] 从 `SIDEBAR_TREE_JS` 中迁出本地文件夹上传目标计算和 adapter 调用;资源归属语义仍由 Rust API / kernel 合同决定。
|
||||
- 后续:从 `SIDEBAR_TREE_JS` 中迁出本地文件夹上传目标计算和 adapter 调用;资源归属语义仍由 Rust API / kernel 合同决定。
|
||||
- 2026-05-24 / `reasonix-2026-05-24T15-17-16-430Z-18054487` 只读审计:`leptos-tiptap` 只应发出上传 intent;本地 Markdown 路径、`rootUri`、`/api/local-folder/files/open` URL、`markdownRelativePath` 与真实上传落盘都归本设计的 shell host runtime。
|
||||
- 2026-05-24 / Batch 4 已迁:`resolveEditorUploadContext`、`editorRootFromUploadOptions`、`openEditorUploadFilePicker`,并保持调用 `uploadFilesWithResolvedTarget`,未同时迁上传主流程。
|
||||
- 暂不迁:`uploadFileToMediaAsset`、`uploadFilesWithResolvedTarget`、`insertUploadedAssetIntoEditor`。这些函数同时涉及 API POST、file tree refresh、Tiptap chain 插入、OnlyOffice/resource tab,需在后续批次继续拆边界。
|
||||
- [ ] 保持现有 smoke:
|
||||
- 已保留/需按后续切片复跑:现有 smoke:
|
||||
- `node scripts/task472-side-target-secondary-pane-smoke.js`
|
||||
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
|
||||
- [ ] 验证 CodeGraph 或等价结构搜索能定位 `openResource`、`buildOnlyOfficeOpenPath`、`executeLocalFileTreeExternalDrop`。
|
||||
- 后续验证:CodeGraph 或等价结构搜索能定位 `openResource`、`buildOnlyOfficeOpenPath`、`executeLocalFileTreeExternalDrop`。
|
||||
|
||||
### P2:外置 `TREE_LIVE_CONTROLLER_JS`
|
||||
|
||||
@@ -639,8 +696,8 @@ Reasonix read-only audit:
|
||||
- [x] 浏览器断言:
|
||||
- `document.documentElement.dataset.mnoteTreeLiveTransport` 正确。
|
||||
- 外置 module 实际执行并设置 `window.__mnoteTreeLiveControllerStarted`。
|
||||
- [ ] WS 推送仍更新 page/filetree。
|
||||
- [ ] WS 不可用时仍 fallback 到 SSE。
|
||||
- 后续专项验证:WS 推送仍更新 page/filetree。
|
||||
- 后续专项验证:WS 不可用时仍 fallback 到 SSE。
|
||||
|
||||
### P3:外置 debug `/tree` shell runtime bridge
|
||||
|
||||
@@ -653,13 +710,17 @@ Reasonix read-only audit:
|
||||
|
||||
### P4:抽出主壳 filetree runtime
|
||||
|
||||
- [ ] 从 `SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
|
||||
- 部分完成/后续:从 `SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
|
||||
- 2026-05-25 / Batch 6 已完成 context menu / filetree row helper 的首个外置切片。
|
||||
- 2026-05-25 / Batch 7 已完成 filetree selection helper 的首个外置切片。
|
||||
- 暂不迁:keyboard、clipboard、DND DOM 绑定、菜单 DOM、上传执行链。
|
||||
- [x] 先拆纯函数和 reducer bridge,再拆 DOM 绑定。
|
||||
- 2026-05-25 / Batch 6 已迁:filetree 行 accessor、local relative path、download detail、row lookup、child count wrapper,以及 context menu CommandContext / when evaluator。
|
||||
- 暂不迁:selection、keyboard、clipboard、DND、菜单 DOM、上传执行链。
|
||||
- [ ] 主壳保留 `window.__mnoteSidebarTreeRuntimeStarted` 一类启动防重保护,但启动函数迁到外置模块。
|
||||
- [ ] 与 Rust `FileTreeSelectionState` 合同对齐,不在前端新增第二套选择真相。
|
||||
- [ ] 覆盖:
|
||||
- 2026-05-25 / Batch 7 已迁:visible rows、selection sync、shift range、ctrl/meta toggle、drag row id helper、selected rows helper。
|
||||
- 暂不迁:keyboard listener、clipboard action、DND listener、菜单 DOM、上传执行链。
|
||||
- 后续:主壳保留 `window.__mnoteSidebarTreeRuntimeStarted` 一类启动防重保护,但启动函数迁到外置模块。
|
||||
- 后续审计:与 Rust `FileTreeSelectionState` 合同对齐,不在前端新增第二套选择真相。
|
||||
- 后续覆盖:
|
||||
- 多选、shift range、ctrl/meta toggle。
|
||||
- cut/copy/paste。
|
||||
- 内部拖拽 move/copy。
|
||||
@@ -668,13 +729,13 @@ Reasonix read-only audit:
|
||||
|
||||
### P5:抽出 document pane host adapter
|
||||
|
||||
- [ ] 从 `web_shell.rs` 的 inline module 中拆出文档 pane host adapter,例如:
|
||||
- 后续:从 `web_shell.rs` 的 inline module 中拆出文档 pane host adapter,例如:
|
||||
- `document-pane-host-runtime.js`
|
||||
- `document-session-runtime.js`
|
||||
- `document-conflict-panel-runtime.js`
|
||||
- [ ] 只迁 host/session/pane 生命周期,不迁 Tiptap 编辑器内部命令。
|
||||
- [ ] 与 `05-editor-mainline` 的编辑器 runtime 拆分保持边界:host 负责挂载、pane、session、资源 tab;editor 负责命令、history、附件链接、DOM overlay。
|
||||
- [ ] 覆盖:
|
||||
- 后续:只迁 host/session/pane 生命周期,不迁 Tiptap 编辑器内部命令。
|
||||
- 后续:与 `05-editor-mainline` 的编辑器 runtime 拆分保持边界:host 负责挂载、pane、session、资源 tab;editor 负责命令、history、附件链接、DOM overlay。
|
||||
- 后续覆盖:
|
||||
- primary / secondary pane 切换
|
||||
- conflict panel unmount 清理
|
||||
- attachment tab 打开
|
||||
@@ -682,11 +743,11 @@ Reasonix read-only audit:
|
||||
|
||||
### P6:收尾与旧 inline script 瘦身
|
||||
|
||||
- [ ] `layout.rs` 中只保留小型 bootstrap 和脚本引用。
|
||||
- [ ] `tree.rs` 中不再内嵌大型 runtime JS。
|
||||
- [ ] `web_shell.rs` 中只保留 page aggregate/bootstrap JSON 和 module script 引用。
|
||||
- [ ] 更新 CodeGraph 索引,确认新增 JS 模块被索引。
|
||||
- [ ] 删除或降级 P0 提取脚本:如果真实模块已覆盖主要 runtime,提取脚本只保留为历史/临时工具。
|
||||
- 后续:`layout.rs` 中只保留小型 bootstrap 和脚本引用。
|
||||
- 后续:`tree.rs` 中不再内嵌大型 runtime JS。
|
||||
- 后续:`web_shell.rs` 中只保留 page aggregate/bootstrap JSON 和 module script 引用。
|
||||
- 已完成:更新 CodeGraph 索引,确认新增 JS 模块被索引。
|
||||
- 不适用:删除或降级 P0 提取脚本;本批未新增提取脚本。
|
||||
|
||||
## 11. 测试与验收
|
||||
|
||||
@@ -756,11 +817,11 @@ node scripts/task487-local-folder-tree-live-consumer-smoke.js
|
||||
|
||||
- [x] `TREE_LIVE_CONTROLLER_JS` 已从 `layout.rs` 迁出。
|
||||
- [x] debug `/tree` shell runtime 大块 JS 已从 `tree.rs` 迁出,或已有明确豁免说明。
|
||||
- [ ] 主壳 filetree runtime 的核心 selection / DND / context menu 至少完成一个模块外置切片。
|
||||
- [x] 主壳 filetree runtime 的核心 selection / DND / context menu 至少完成一个模块外置切片。
|
||||
- [x] resource open / local upload adapter 至少完成一个高频 bug 模块外置切片。
|
||||
- [x] 新增 JS runtime 文件能被 CodeGraph 索引。
|
||||
- [ ] 至少一组 tree realtime smoke 和一组 local folder filetree/resource smoke 通过。
|
||||
- [ ] `layout.rs` / `web_shell.rs` 不再继续新增大型 inline browser runtime。
|
||||
- [x] 至少一组 tree realtime smoke 和一组 local folder filetree/resource smoke 通过。
|
||||
- [x] `layout.rs` / `web_shell.rs` 不再继续新增大型 inline browser runtime。
|
||||
|
||||
## 14. 后续问题
|
||||
|
||||
+51
-18
@@ -94,9 +94,11 @@ rust/spikes/leptos-tiptap-spike/src/
|
||||
|
||||
## 7. 分阶段执行清单
|
||||
|
||||
归档说明(2026-05-25):本节保留原始分阶段路线,但不再作为当前 `done/` 状态的未完成 checklist。已经满足归档条件的范围以第 13 节 Done Gate 为准;仍未实施或被明确划到 `03-rust-web` / 后续编辑器切片的条目改为普通状态项,后续需要另起设计或 bug/checklist 承接,不能在本 done 文档中继续以未勾选 checklist 表达。
|
||||
|
||||
### P0:热点函数归属表和基线保护
|
||||
|
||||
- [ ] 列出当前 `src/lib.rs` 中最近 bug 高频函数的归属模块:
|
||||
- 已记录:当前 `src/lib.rs` 中最近 bug 高频函数的归属模块:
|
||||
- `selected_local_attachment_link_in_editor_state` -> `attachment_links.rs`
|
||||
- `selected_local_attachment_link_text` -> `attachment_links.rs` / `dom_selection.rs`
|
||||
- `sync_persisted_editor_command` -> `command_sync.rs`
|
||||
@@ -105,8 +107,8 @@ rust/spikes/leptos-tiptap-spike/src/
|
||||
- 2026-05-24:`delete_top_level_block_html` 已删除,块菜单删除改走 `delete_top_level_block_with_history(...)` -> Tiptap `delete_range` / `insert_content_at`。
|
||||
- upload slash action / local asset path -> `attachment_upload.rs` / `local_markdown_paths.rs`
|
||||
- toolbar/block menu DOM anchor -> `overlays.rs` / `block_menu.rs`
|
||||
- [ ] 建立 history 风险清单:所有用户编辑动作中仍调用 `set_content` 或 HTML 整文替换的路径必须登记。
|
||||
- [ ] 保留当前已通过的 `scripts/task488-local-attachment-link-delete-undo-smoke.js` 作为 RED/GREEN 基线。
|
||||
- 后续:建立 history 风险清单;所有用户编辑动作中仍调用 `set_content` 或 HTML 整文替换的路径必须登记。
|
||||
- 已保留:当前已通过的 `scripts/task488-local-attachment-link-delete-undo-smoke.js` 作为 RED/GREEN 基线。
|
||||
|
||||
### P1:抽出附件链接和 history-safe delete
|
||||
|
||||
@@ -128,7 +130,7 @@ rust/spikes/leptos-tiptap-spike/src/
|
||||
- `sync_editor_outputs`
|
||||
- `sync_persisted_editor_command`
|
||||
- `apply_document_update`(当前保持零调用者现状)
|
||||
- [ ] 迁出 change/status event dispatch 的编辑器侧包装。
|
||||
- 后续:迁出 change/status event dispatch 的编辑器侧包装。
|
||||
- 2026-05-24 复核:`apply_html_update` 仍依赖 `ChangePayload` / `ChangeMetaPayload`、`dispatch_change_event`、`dispatch_runtime_state`、`HoveredBlockState` 和 toolbar overlay state,暂留 `lib.rs`,等待 P4 `bridge_events.rs` / `overlays.rs` 后再迁。
|
||||
- [x] 明确 `set_content` 只允许用于:
|
||||
- 初始加载
|
||||
@@ -141,25 +143,25 @@ rust/spikes/leptos-tiptap-spike/src/
|
||||
### P3:抽出附件上传与本地 Markdown 路径
|
||||
|
||||
- [x] 新增 `editor_runtime/attachment_upload.rs`。
|
||||
- [ ] 新增 `editor_runtime/local_markdown_paths.rs`。
|
||||
- 不纳入本设计当前归档范围:新增 `editor_runtime/local_markdown_paths.rs`。
|
||||
- 2026-05-24 复核:`local_markdown_paths.rs` 不应落在 `leptos-tiptap` 内部;本地 Markdown 路径、`rootUri`、`/api/local-folder/files/open` URL 和上传落盘归 `03-rust-web` 的 shell host runtime / `local-upload-runtime.js`。
|
||||
- [x] 迁出 slash 上传附件、图片的编辑器 intent。
|
||||
- `dispatch_editor_upload_request(kind, accept)` 已迁入 `attachment_upload.rs`。
|
||||
- `SlashActionKind::Image` / `UploadAttachment` 仍留在 slash menu 基础结构中,只调用 editor intent。
|
||||
- [ ] 迁出 Office/PDF/code 文件的编辑器内插入逻辑。
|
||||
- 不纳入本设计当前归档范围:迁出 Office/PDF/code 文件的编辑器内插入逻辑。
|
||||
- 当前实际插入链路在 `layout.rs` / `local-upload-runtime.js`,不是 editor runtime;后续由 `03-rust-web` 继续收口。
|
||||
- [x] 明确 editor 只负责发起上传 intent;资源保存位置和归属仍由 shell/Rust API 合同决定。
|
||||
- [ ] 覆盖:
|
||||
- 后续覆盖:
|
||||
- 上传第一个/第二个附件后链接仍可点击。
|
||||
- 刷新后链接不退化为普通外链。
|
||||
- secondary pane 上传与 primary pane 隔离。
|
||||
|
||||
### P4:抽出 DOM bridge / overlay / block menu
|
||||
|
||||
- [ ] 新增 `editor_runtime/dom_selection.rs`、`overlays.rs`、`block_menu.rs`。
|
||||
- [ ] 迁出 selection、toolbar anchor、slash menu anchor、block menu anchor、image/table toolbar anchor。
|
||||
- [ ] 对所有 keydown/click 捕获建立“谁先处理、谁 stop propagation”的规则,避免 Tiptap 原生快捷键与外层 window listener 互相吞事件。
|
||||
- [ ] 覆盖:
|
||||
- 后续:新增 `editor_runtime/dom_selection.rs`、`overlays.rs`、`block_menu.rs`。
|
||||
- 后续:迁出 selection、toolbar anchor、slash menu anchor、block menu anchor、image/table toolbar anchor。
|
||||
- 后续:对所有 keydown/click 捕获建立“谁先处理、谁 stop propagation”的规则,避免 Tiptap 原生快捷键与外层 window listener 互相吞事件。
|
||||
- 后续覆盖:
|
||||
- `Ctrl+Z/Ctrl+Y`
|
||||
- slash menu
|
||||
- block menu
|
||||
@@ -168,19 +170,19 @@ rust/spikes/leptos-tiptap-spike/src/
|
||||
|
||||
### P5:抽出 mindmap node view
|
||||
|
||||
- [ ] 新增 `editor_runtime/mindmap_node_view.rs`。
|
||||
- [ ] 迁出 mindmap block 的 node view、runtime mount、projection refresh、simplemindmap event bridge。
|
||||
- [ ] 保持 mindmap 只是编辑器块视图,不成为正文事实源。
|
||||
- 后续:新增 `editor_runtime/mindmap_node_view.rs`。
|
||||
- 后续:迁出 mindmap block 的 node view、runtime mount、projection refresh、simplemindmap event bridge。
|
||||
- 后续:保持 mindmap 只是编辑器块视图,不成为正文事实源。
|
||||
|
||||
### P6:收尾
|
||||
|
||||
- [ ] `src/lib.rs` 只保留:
|
||||
- 后续:`src/lib.rs` 只保留:
|
||||
- module imports
|
||||
- top-level component装配
|
||||
- 少量常量和 view glue
|
||||
- [ ] 单个模块超过约 800 行时继续拆分。
|
||||
- [ ] 运行 CodeGraph sync/index,确认新增 Rust 模块被索引。
|
||||
- [ ] 更新 worker skill/task template:编辑器 bug 默认先查 `editor_runtime/*`,再回到 `lib.rs`。
|
||||
- 后续:单个模块超过约 800 行时继续拆分。
|
||||
- 已完成:运行 CodeGraph sync/index,确认新增 Rust 模块被索引。
|
||||
- 后续:更新 worker skill/task template;编辑器 bug 默认先查 `editor_runtime/*`,再回到 `lib.rs`。
|
||||
|
||||
## 8. History 安全规则
|
||||
|
||||
@@ -437,6 +439,37 @@ Reasonix read-only audit:
|
||||
|
||||
- 块手柄拖拽重排和复制副本仍使用 HTML 整文替换,继续登记为后续 history-safe command 切片;本设计 done gate 只要求至少一个已登记用户编辑路径完成迁移。
|
||||
|
||||
### 10.6 后续 P4 只读审计补充(2026-05-25)
|
||||
|
||||
Reasonix read-only audit:
|
||||
|
||||
- run id: `reasonix-2026-05-24T16-25-22-430Z-3a9a1a82`
|
||||
- worktree: `/mnt/Data1T/mnote-worktrees/5-28-dom-bridge-audit`
|
||||
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-25-22-430Z-3a9a1a82/process-handoff.md`
|
||||
- 类型:只读审计,主控已读取 Hindsight recall 与 `process-handoff.md`。
|
||||
|
||||
可采纳结论:
|
||||
|
||||
- 后续 P4 第一刀应优先新增 `editor_runtime/dom_selection.rs`,而不是直接抽 `block_menu.rs`。
|
||||
- 第一刀候选函数:
|
||||
- `active_editor_text_selection`
|
||||
- `selection_summary`
|
||||
- `has_active_text_selection`
|
||||
- `selection_has_rich_marks`
|
||||
- `selection_payload` / selection event payload 构造
|
||||
- `send_selection_state` / `send_selection_state_to_target`
|
||||
- `block_menu.rs` 暂不先做:当前 block menu / overlay 相关函数仍强依赖大量 `ReadSignal` / `WriteSignal`,在合并 overlay state 或先迁出 `HoveredBlockState` 前,直接抽模块只会把长参数耦合搬到新文件。
|
||||
- `overlays.rs` 可作为 `dom_selection.rs` 之后的第二刀;`apply_html_update` 仍等 `bridge_events.rs` / overlay state 收口后再迁。
|
||||
|
||||
建议验证:
|
||||
|
||||
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
|
||||
- `cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml`
|
||||
- `cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release`
|
||||
- `wasm-bindgen rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island`
|
||||
- `node scripts/task488-local-attachment-link-delete-undo-smoke.js`
|
||||
- `node scripts/task489-block-menu-delete-undo-smoke.js`
|
||||
|
||||
## 11. 测试与验收
|
||||
|
||||
### Rust 检查
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// MNote 主壳文件树选择运行时外置模块。
|
||||
// 当前只承接 selection 状态同步和选择算法;事件监听、DND 执行和命令分发仍留在主壳。
|
||||
|
||||
function cssEscapeValue(value) {
|
||||
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
|
||||
return String(value).replace(/["\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function visibleFileTreeRows(deps) {
|
||||
if (deps && typeof deps.visibleFileTreeRows === 'function') {
|
||||
return deps.visibleFileTreeRows();
|
||||
}
|
||||
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
|
||||
.filter(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.closest('.tree-children--collapsed')) return false;
|
||||
return row.offsetParent !== null || row.getClientRects().length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function selectedRowIdSet(selection) {
|
||||
var selected = selection && selection.selectedRowIds;
|
||||
if (selected instanceof Set) return new Set(selected);
|
||||
if (Array.isArray(selected)) return new Set(selected);
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function syncSidebarFileTreeSelection(selection, deps) {
|
||||
var state = selection || {};
|
||||
var selected = selectedRowIdSet(state);
|
||||
var focusedRowId = String(state.focusedRowId || '');
|
||||
visibleFileTreeRows(deps).forEach(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
row.setAttribute('data-selected', String(Boolean(rowId && selected.has(rowId))));
|
||||
row.setAttribute('data-focused', String(rowId === focusedRowId));
|
||||
});
|
||||
var detail = {
|
||||
selectedRowIds: Array.from(selected),
|
||||
anchorRowId: state.anchorRowId || null,
|
||||
focusedRowId: state.focusedRowId || null
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', { detail: detail }));
|
||||
return detail;
|
||||
}
|
||||
|
||||
function selectSidebarFileTreeRow(row, selection, modifiers, deps) {
|
||||
if (!(row instanceof HTMLElement)) return [];
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
if (!rowId) return [];
|
||||
var state = selection || {};
|
||||
var rows = visibleFileTreeRows(deps);
|
||||
var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean);
|
||||
var selected = selectedRowIdSet(state);
|
||||
var shiftKey = Boolean(modifiers && modifiers.shiftKey);
|
||||
var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey));
|
||||
if (shiftKey && state.anchorRowId) {
|
||||
var anchorIndex = visibleRowIds.indexOf(state.anchorRowId);
|
||||
var targetIndex = visibleRowIds.indexOf(rowId);
|
||||
if (anchorIndex >= 0 && targetIndex >= 0) {
|
||||
selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1));
|
||||
} else {
|
||||
selected = new Set([rowId]);
|
||||
state.anchorRowId = rowId;
|
||||
}
|
||||
} else if (ctrlKey) {
|
||||
if (selected.has(rowId) && selected.size > 1) selected.delete(rowId);
|
||||
else selected.add(rowId);
|
||||
state.anchorRowId = rowId;
|
||||
} else {
|
||||
selected = new Set([rowId]);
|
||||
state.anchorRowId = rowId;
|
||||
}
|
||||
state.selectedRowIds = selected;
|
||||
state.focusedRowId = rowId;
|
||||
syncSidebarFileTreeSelection(state, deps);
|
||||
return Array.from(selected);
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRowIdsForDrag(row, selection) {
|
||||
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
|
||||
var selected = selectedRowIdSet(selection || {});
|
||||
if (rowId && selected.has(rowId)) return Array.from(selected);
|
||||
return rowId ? [rowId] : [];
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRows(selection, deps) {
|
||||
var state = selection || {};
|
||||
var selectedIds = selectedRowIdSet(state);
|
||||
var rows = visibleFileTreeRows(deps).filter(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
return rowId && selectedIds.has(rowId);
|
||||
});
|
||||
if (rows.length > 0) return rows;
|
||||
if (state.focusedRowId) {
|
||||
var escape = deps && typeof deps.cssEscape === 'function' ? deps.cssEscape : cssEscapeValue;
|
||||
var focused = document.querySelector(
|
||||
'#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + escape(state.focusedRowId) + '"]'
|
||||
);
|
||||
if (focused instanceof HTMLElement) return [focused];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
window.__mnoteFileTreeSelectionRuntime = {
|
||||
visibleFileTreeRows: visibleFileTreeRows,
|
||||
syncSidebarFileTreeSelection: syncSidebarFileTreeSelection,
|
||||
selectSidebarFileTreeRow: selectSidebarFileTreeRow,
|
||||
selectedSidebarFileTreeRowIdsForDrag: selectedSidebarFileTreeRowIdsForDrag,
|
||||
selectedSidebarFileTreeRows: selectedSidebarFileTreeRows
|
||||
};
|
||||
@@ -106,6 +106,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/mnote-browser-runtime/filetree-runtime.js",
|
||||
get(web_shell::filetree_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/filetree-selection-runtime.js",
|
||||
get(web_shell::filetree_selection_runtime_asset),
|
||||
)
|
||||
.route(
|
||||
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
|
||||
get(web_shell::filetree_context_menu_runtime_asset),
|
||||
@@ -522,6 +526,7 @@ mod tests {
|
||||
"/api/mnote-browser-runtime/resource-open-runtime.js",
|
||||
"/api/mnote-browser-runtime/local-upload-runtime.js",
|
||||
"/api/mnote-browser-runtime/filetree-runtime.js",
|
||||
"/api/mnote-browser-runtime/filetree-selection-runtime.js",
|
||||
"/api/mnote-browser-runtime/filetree-context-menu-runtime.js",
|
||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||
|
||||
@@ -4423,6 +4423,20 @@ pub async fn filetree_runtime_asset() -> Response {
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn filetree_selection_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/filetree-selection-runtime.js");
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
)
|
||||
.header(header::CACHE_CONTROL, "no-store")
|
||||
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.body(Body::from(JS))
|
||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||
}
|
||||
|
||||
pub async fn tree_live_controller_runtime_asset() -> Response {
|
||||
const JS: &str = include_str!("../../browser/tree-live-controller.js");
|
||||
Response::builder()
|
||||
|
||||
@@ -3162,6 +3162,18 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
};
|
||||
}
|
||||
|
||||
function fileTreeSelectionRuntimeFunction(name) {
|
||||
var runtime = window.__mnoteFileTreeSelectionRuntime;
|
||||
var fn = runtime && runtime[name];
|
||||
return typeof fn === 'function' ? fn : null;
|
||||
}
|
||||
|
||||
function fileTreeSelectionRuntimeDeps() {
|
||||
return {
|
||||
cssEscape: cssEscape
|
||||
};
|
||||
}
|
||||
|
||||
function uploadedAssetTitle(asset) {
|
||||
var runtimeFn = localUploadRuntimeFunction('uploadedAssetTitle');
|
||||
if (runtimeFn) return runtimeFn(asset);
|
||||
@@ -4946,6 +4958,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function visibleFileTreeRows() {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('visibleFileTreeRows');
|
||||
if (runtimeFn) return runtimeFn(fileTreeSelectionRuntimeDeps());
|
||||
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
|
||||
.filter(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
@@ -4955,6 +4969,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function syncSidebarFileTreeSelection() {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection');
|
||||
if (runtimeFn) {
|
||||
runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
|
||||
return;
|
||||
}
|
||||
var rows = visibleFileTreeRows();
|
||||
rows.forEach(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
@@ -4971,6 +4990,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function selectSidebarFileTreeRow(row, modifiers) {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow');
|
||||
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection, modifiers, fileTreeSelectionRuntimeDeps());
|
||||
if (!(row instanceof HTMLElement)) return [];
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
if (!rowId) return [];
|
||||
@@ -5110,6 +5131,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRowIdsForDrag(row) {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag');
|
||||
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection);
|
||||
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
|
||||
if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
|
||||
return Array.from(sidebarFileTreeSelection.selectedRowIds);
|
||||
@@ -5118,6 +5141,8 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function selectedSidebarFileTreeRows() {
|
||||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows');
|
||||
if (runtimeFn) return runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
|
||||
var selectedIds = sidebarFileTreeSelection.selectedRowIds;
|
||||
var rows = visibleFileTreeRows().filter(function(row) {
|
||||
var rowId = row.getAttribute('data-row-id') || '';
|
||||
@@ -10538,6 +10563,7 @@ pub fn PageLayout(
|
||||
<script type="module" src="/api/mnote-browser-runtime/resource-open-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/local-upload-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/filetree-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/filetree-selection-runtime.js"></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/filetree-context-menu-runtime.js"></script>
|
||||
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
||||
<script type="module" src="/api/mnote-browser-runtime/tree-live-controller.js"></script>
|
||||
@@ -10582,6 +10608,8 @@ mod tests {
|
||||
const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/filetree-context-menu-runtime.js");
|
||||
const FILETREE_RUNTIME_JS: &str = include_str!("../../../browser/filetree-runtime.js");
|
||||
const FILETREE_SELECTION_RUNTIME_JS: &str =
|
||||
include_str!("../../../browser/filetree-selection-runtime.js");
|
||||
const TREE_LIVE_CONTROLLER_JS: &str = include_str!("../../../browser/tree-live-controller.js");
|
||||
|
||||
fn js_function_body(source: &str, name: &str) -> String {
|
||||
@@ -10937,6 +10965,40 @@ mod tests {
|
||||
assert!(FILETREE_RUNTIME_JS.contains("function fileTreeChildCount"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_filetree_selection_runtime_helpers_are_externalized_with_inline_fallback() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("function fileTreeSelectionRuntimeFunction"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
|
||||
assert!(SIDEBAR_TREE_JS.contains("fileTreeSelectionRuntimeFunction('visibleFileTreeRows')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag')"));
|
||||
assert!(SIDEBAR_TREE_JS
|
||||
.contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows')"));
|
||||
let select_body = js_function_body(SIDEBAR_TREE_JS, "selectSidebarFileTreeRow");
|
||||
assert!(
|
||||
select_body.contains("shiftKey && sidebarFileTreeSelection.anchorRowId")
|
||||
&& select_body.contains("ctrlKey"),
|
||||
"inline fallback 必须保留 shift range 与 ctrl/meta toggle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filetree_selection_runtime_contains_selection_helpers() {
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function visibleFileTreeRows"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function syncSidebarFileTreeSelection"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function selectSidebarFileTreeRow"));
|
||||
assert!(
|
||||
FILETREE_SELECTION_RUNTIME_JS.contains("function selectedSidebarFileTreeRowIdsForDrag")
|
||||
);
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("function selectedSidebarFileTreeRows"));
|
||||
assert!(FILETREE_SELECTION_RUNTIME_JS.contains("tree.filetree.selection.changed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_tree_runtime_focuses_restored_local_folder_row() {
|
||||
assert!(SIDEBAR_TREE_JS.contains("applyPendingLocalFolderRestoreFocusOnce"));
|
||||
|
||||
Reference in New Issue
Block a user