# Agent Note: 输入框的编辑自带它所作用的范围

Status: implemented
Archived: 2026-08-20

[English](2026-08-20-composer-edit-range-from-selection.md) | 中文

## 问题

输入机器靠一个编辑范围来对齐引用 occurrence：范围之前的条目右移，之后的条目不动，被范围相交的条目失去结构化身份、以普通草稿文本留在原地。最后一条是"在引用内部编辑"的刻意含义。

普通打字不提供范围。受控 textarea 的 change 事件只带结果字符串，于是机器靠扫描两份草稿的公共前后缀来还原范围。只要插入的文本与它落点处的文本重复，这个还原就是有歧义的，而贪心扫描永远以同一种方式消解歧义：把编辑尽量往后滑。

引用渲染为 `@` 加标签，所以紧挨着引用前面打一个 `@` 恰好构成这种撞车。用户在引用自身的偏移处插入；扫描报告的插入位置晚一个字符，落在引用内部；reconcile 执行相交规则，删掉这个 occurrence。删除这类引用前面的一个字符会以同样方式滑动。

此时草稿看上去仍然正确，却已不携带任何结构化引用，提交走的是无 occurrence 的那条路，把草稿原样发出。宿主收到的是给人看的标签而不是所有者的模型形式，什么也解析不出来。专为阻止这种降级而存在的序列化守卫从不运行，因为它只在还有 occurrence 需要序列化时才触发。

这条路径是在引用[变成字面内联文本](../feature/2026-07-27-web-file-and-session-references.md)之后才可达的。此前一个引用占据一个 `U+FFFC`——任何按键都打不出的字符，扫描无从撞车。

## 决策

`InputBar` 在 `beforeinput` 期间记录 textarea 的 selection 与 `inputType`，并把由此得到的范围传给 `setDraft`；机器本就接受该参数，并优先于自身的扫描。textarea 没有别的途径暴露这次编辑——`getTargetRanges()` 对表单控件返回空。

替换一段选区的编辑会报告那段 selection，它直接就是范围；插入长度是扣除被替换范围后草稿增长的量。折叠光标的删除不替换任何东西、只报告光标本身，因此它的范围来自 `inputType` 指明的方向加上草稿实际减少的字符数。这个数量是**测量**得到而非假定为 1，因为一次折叠手势同样可能删掉一个多码元字形、一个词或一整行。Chromium、WebKit 与 Firefox 对 `deleteContentBackward` 和 `deleteContentForward` 都报告折叠光标，三者由此推导出相同的范围。

只有 `insert` 与 `delete` 两族会被记录。历史回放报告的是光标当时碰巧所在的位置，那会通过全部校验却指向错误区间；忽略它即让该路径留在扫描上。

记录只消费一次即清空。记录的草稿长度与 change 报告的草稿不符、selection 越过草稿末尾、选区之上出现光标无法解释的收缩、以及方向不明的删除，这几种情况都不产出范围，机器回落到扫描。粘贴以及边界处的 Backspace 与 Delete 手势本就自带范围，未受影响。

## 测试

组件测试覆盖在引用正前方输入触发字符、折叠 Backspace、折叠 Delete、折叠整词删除，以及选区替换式删除，每种都断言 occurrence 在右移后的偏移处存活。折叠类用例在扫描还原的范围下失败，整词用例在固定一字符步长的实现下失败。

组装层浏览器场景以真实按键对已发布组合驱动同样的手势——那是唯一能观察到引擎为这些手势报告何种范围的地方；其 golden 投影的是 backdrop 的分段，因为装饰层是 aria-hidden 的，无障碍树看不到 chip。经浏览器 IME 路径驱动的真实组合在每个中间态都把组合段报告为 selection，引用在每一步都存活。

## 备选方案

**用编辑后的光标位置消解扫描的歧义。** 光标能钉住若干文本等价读法中真正发生的那一种，而 change 事件本就携带它。拒绝：在已有精确事实可用时仍保留一次重建，而且它根本无法把"替换一段选区"拆成删除与插入两半。

**给引用一个按键打不出的前导标记。** 用私有区字符取代字面 `@`，在表示层消除撞车，而粘贴文本的剔除名单本就点名了那个区段。拒绝：为了换取一个精确范围本就能直接换到的东西，却重新引入一个必须在所有序列化、选择和无障碍路径上剥离的字符，而且普通打字仍会因为其他原因继续重建自己的范围。

**放宽 reconcile，让范围只触及边缘时保留 occurrence。** 拒绝：误判出的偏移严格落在引用内部而非边界上，规则放宽够不到这个缺陷，只会让相交规则本身更含糊。

## 后果

每一次经 `onChange` 到达的原生 textarea 编辑现在都指明它作用的范围，因此 occurrence 偏移跟随的是真实发生的编辑，而不是某个仅仅与结果字符一致的编辑。源自 facade 而非 DOM 的草稿写入——`insertText` 和命令 token 的替换等——仍不带范围并保留扫描，上述各类回落同样如此。

输入框由此依赖 `beforeinput` 先于每次取值变更发生，并依赖 `inputType` 指明折叠删除的方向。未来任何绕过其中之一改写取值的编辑路径会静默回落到扫描而不是出错，也就是说失效模式退回旧行为，而不是一个错误的范围。
