
Slate v2 Range Refs 证明计划以事务感知的持久化 Range 引用锚定批注【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文以仓库计划文档 2026-04-03-slate-v2-range-refs-proof-plan.md 为主体完整还原slate-v2中第一个“持久化 Range 引用range ref缝seam”的设计与落地过程。该计划解决的核心问题是批注comment锚点、注解annotation覆盖层这类需要长期存活的位置引用不应依赖会随编辑随时失效的临时 selection 状态。读完本文你将掌握Editor.rangeRef(editor, range, options?)的 API 形态、RangeRefAffinity四种亲和性语义、事务感知的 ref 发布机制以及它如何用“窄测试先行”的证明方法在packages/slate-v2中落地并通过架构评审。背景投影证明解决了临时订阅却没解决持久锚点slate-v2的前序工作完成了“投影projection证明”让本地覆盖层overlay可以跟随当前 selection 做即时渲染。但计划文档开篇明确指出它有一个未覆盖的空档投影机制擅长“临时、跟随当前状态”的覆盖层订阅但它不负责“跨编辑存活”的锚点批注锚点comment anchors在编辑发生后会失效——文本被插入、节点被移动后旧的锚点位置不再指向原来的内容注解覆盖层只能跟随当前 selection无法记住“用户当时圈选的那段文字”结论是slate-react-v2在“临时投影”上表现良好但在“持久化”上存在缺陷。换句话说覆盖层渲染与锚点持久化是两个正交的能力前者解决“现在画什么”后者解决“以后还能找到它”。这正是本计划要补上的第二块拼图。工作假设与首个切片First Cut最小而诚实的 API 面计划给出的最小首切假设非常收敛只包含四个要素Editor.rangeRef(editor, range)—— 在编辑器中为某个 range 创建持久引用返回的 ref 对象暴露当前值Range | nullref 会随受支持的操作族增量更新incremental updateunref()干净地解绑引用。计划还刻意留了一个口子书签bookmark序列化是否纳入本次范围取决于第一个证明是否强制需要——除非证明本身迫使它否则推迟到后续跟进。这是典型的“最小诚实切片”策略先解决核心问题不为猜测中的需求提前扩权。公开 API 与语义决策Editor.rangeRef(editor, range, options?) type RangeRef type RangeRefAffinity forward | backward | outward | inward | null语义上有三个关键决策其中“默认亲和性”是本次计划与 legacy Slate 最大的分叉点默认亲和性取inward。计划明确指出对持久化批注锚点而言inward优于 legacy Slate 默认的forward。原因很直观——批注锚点希望“咬住”被圈选的内容边界当编辑发生在边界上时向内收缩比向外扩张更能保持锚点与内容的归属关系ref 状态是事务感知的transaction-aware并在提交commit时发布publish最新值unref()返回最新的逻辑 range 并解绑 ref——这是“取回最新值 释放资源”的二合一操作。首轮证明的子集边界为了把问题域压到最小第一个证明只覆盖当前 v2 文本证明子集range 只落在文本点上text points at[block, 0]支持的操作族insert_text、insert_fragment、move_nodeset_selection作为 ref 变换的 no-opselection 变化不应移动持久引用显式替换explicit replacement会按意图清空或解绑 ref。这个边界设计本身就是一种工程纪律把操作族先收敛到三类验证 ref 变换机制成立再谈扩展。分阶段执行从红测试到架构评审计划把执行拆成五个阶段节奏紧凑Phase 0梳理 legacy 行为、当前 v2 缝seams与 issue 压力确定改动落点Phase 1为一个持久的 range-ref 行为写一条窄红测试narrow red testPhase 2实现能通过该测试的最小核心缝Phase 3仅当测试迫使时才扩展下一批必需的操作族Phase 4验证、deslop清理冗余/脏代码、复验、架构评审、收尾。其中 Phase 3 的“仅当测试迫使时”only if tests force it与 Phase 1 的“窄测试先行”是同一方法论的两面需求由测试驱动范围由失败驱动避免一次性铺开过大的实现面。落地证据契约测试覆盖点2026-04-03 当天该证明即已落地到.tmp/slate-v2并补充了聚焦的契约测试覆盖以下七类行为commit 边界上的值发布commit-boundary publicationmove-node 的重定位move-node rebasing显式多块 fragment 的重定位explicit multi-block fragment rebasing显式替换的失效explicit replacement invalidation默认 inward 边界行为unref()解绑语义活跃事务期间的失效invalidation during an active transaction。这七条覆盖点恰好一一对应计划中“首轮证明子集”的边界既验证了受支持操作族的正确变换也验证了事务感知与解绑的语义完整性。验证命令与评审过程计划记录了完整的验证证据链全部以 mocha babel 注册器在packages/slate-v2/test/下执行zsh -lc yarn mocha --require ./config/babel/register.cjs ./packages/slate-v2/test/range-ref-contract.ts zsh -lc yarn mocha --require ./config/babel/register.cjs ./packages/slate-v2/test/snapshot-contract.ts zsh -lc yarn mocha --require ./config/babel/register.cjs ./packages/slate-v2/test/clipboard-contract.ts zsh -lc yarn workspace slate-react-v2 test此外要求变更文件的 LSP 诊断为0无类型/语法错误。架构评审环节值得一提评审最初拒绝了三个问题修复后才通过通用 move-node 重定位generic move-node rebasing——不能只处理特定路径形态需要覆盖一般情形被中止事务中创建的 ref 的回滚安全性rollback safety for refs created in aborted transactions——事务被中止时事务内新建的 ref 必须被正确清理不能残留脏状态replaceSnapshot()之后的 runtime-id 游标复用runtime-id cursor reuse——快照替换后游标复用不能导致 id 冲突或引用错乱。修复后最终架构评审结论为APPROVE。这次评审互动还沉淀了一条可复用经验记录在 2026-04-03-slate-v2-range-refs-must-be-transaction-aware-and-default-inward.md仓库内对应docs/solutions逻辑错误类目下的同名文档range refs 必须事务感知且默认 inward。Deslop 发现的真实 Bug计划特别记录了一次 deslop 清理带来的真实收益unref()在 draft ref 值已经是null时可能读到过期的已提交状态stale committed state。这是一个典型的“读取路径与写入路径不同步”缺陷——解绑操作在草稿态为空的场景下没有取到最新值。修复后unref()的返回值语义才与“取回最新逻辑 range”的契约一致。这说明 deslop 不是单纯删代码而是一次针对状态一致性的审查。仓库中的实现印证API 挂载与类型定义在 create-editor.ts 中可以看到 editor 实例上 ref 相关 API 的统一挂载方式pointRef: bindFirst(createPointRef, editor), pointRefs: bindFirst(getPointRefs, editor), rangeRef: bindFirst(createRangeRef, editor), rangeRefs: bindFirst(getRangeRefs, editor),对应的类型声明位于 editor-api.ts其中对rangeRef的注释准确概括了本计划的语义目标Create a mutable ref for aTRangeobject, which will stay in sync as new operations are applied to the editor.RangeRef 的运行时形态location-ref.ts 定义了RangeRef的类型形态与本计划中的 API 一一对应export type RangeRef { affinity: backward | forward | inward | outward | null; current: TRange | null; unref: () TRange | null; };该文件还展示了同一套“ref 随操作变换”机制也覆盖PathReflocation-ref.ts与PointReflocation-ref.ts且PathRefApi.transform有一个值得注意的细节当路径被移除导致current变为null时会自动调用unref()完成自我解绑location-ref.ts。契约测试的仓库级证据location-ref.spec.ts 提供了与计划“契约覆盖”同源的测试证据其中 range refs 的用例location-ref.spec.ts验证了inward亲和性在split_node上的行为一个锚点/焦点都在[0, 0]offset 1 的零宽 range经过split_node变换后落在新块[0, 1]offset 0 ——即分裂点后的位置且unref未被触发。同一文件的另外两个用例分别验证了 path ref 的自我解绑location-ref.spec.ts与已为null的 ref 在变换中被忽略location-ref.spec.ts。与 legacy Slate 的关系值得注意的是当前仓库中的packages/slate将变换语义委托给了 legacy SlateRangeRefApi.transform SlateRangeRef as any见 location-ref.ts而本计划的目标是在slate-v2中建立自己的、事务感知的ref 缝。这正是计划的价值所在不满足于“委托能跑”而是为 v2 的持久化语义commit 发布、draft 回滚、inward 默认亲和性建立独立的、可测试的契约。可复用方法论三个关键教训从这份计划中可以提炼出对任何富文本编辑器内核改造都适用的方法锚点持久化必须与渲染投影分离设计。投影解决“现在画什么”ref 解决“编辑后还能找到它”二者缺一不可默认亲和性不是小事。inward与forward的选择直接决定批注锚点在边界编辑下的归属行为面向持久批注的引擎应优先考虑inward事务感知是硬性要求。draft ref 的发布必须发生在 commit 边界且必须处理“事务中止时新建 ref 的回滚”否则撤销/回滚路径会产生悬空引用。本计划中“commit-boundary publication”“invalidation during an active transaction”两条契约覆盖以及评审中“rollback safety”修复都是这一教训的直接产物。后续建议本计划已将第一个持久 range-ref 缝落地并通过架构评审后续工作可从以下方向推进均以仓库内既有证据为基础将受支持操作族从insert_text/insert_fragment/move_node扩展到remove_text、merge_node、split_node等剩余族并为每个族补充契约测试可参照 location-ref.spec.ts 的用例风格将书签bookmark序列化纳入考虑使 ref 可以跨快照/持久化边界存活——计划明确将其作为“可能的后继跟进”为批注comment与注解annotation插件接入Editor.rangeRef替换当前依赖临时 selection 的锚点方案从而在slate-react-v2中真正补齐“持久化优于临时投影”的短板将“range refs 必须事务感知且默认 inward”沉淀的经验见 2026-04-03-slate-v2-range-refs-must-be-transaction-aware-and-default-inward.md推广到其他持久化位置引用如 path ref、point ref的实现中。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考