zh CN 增量解析 - chiba233/yumeDSL GitHub Wiki
parseStructural(...) 已经很快,但在编辑器场景里,每敲一个键就跑一遍全量 structural 扫描——这开销你肯定不想要。这一页聊的就是
yume-dsl-rich-text 提供的增量 structural 缓存 API,帮你省掉那些没必要的重复计算。
createIncrementalSession(...)/parseIncremental(...)是稳定的公开 API。 增量策略和启发式仍可能继续优化,但 session-first 合约、结果字段和全量回退语义属于稳定接入面。 生产环境依然建议固定版本,并保留一条你自己验证过的全量重建兜底路径。
增量 API 帮你维护两样东西:
-
tree:强制开启trackPositions: true的StructuralNode[] -
zones:由buildZones(tree)得到的Zone[]
它跟 tree-sitter 那种节点级复用不是一回事。核心思路是"把脏区重新解析一遍,没碰到的 zone 直接复用"。
- 文档比较大(几十 KB 往上),而且你在编辑器里需要按键级别反复跑 structural parse。
- 你想维护一份稳定的
StructuralNode[]+Zone[]缓存,拿来驱动高亮、outline、lint、增量管线之类的。 - 你想把 structural 缓存和
yume-dsl-token-walker的parseSlice(...)组合起来,实现"只重新解析被编辑区域"的TextToken[]生成。
如果你已经能定位到需要重解析的区域,而且只需要做 substring parse,那单用 parseSlice(...) 往往就够了,不用搬出整套增量机制。
直接抄:
import {createIncrementalSession} from "yume-dsl-rich-text";
const handlers = {/* ... */};
const session = createIncrementalSession(initialSource, {handlers});
// 每次编辑
const result = session.applyEdit(
{startOffset, oldEndOffset, newText},
newSource,
);
render(result.doc.tree);如果你要局部刷新,改用 applyEditWithDiff:
const result = session.applyEditWithDiff(
{startOffset, oldEndOffset, newText},
newSource,
undefined,
{maxMilliseconds: 4},
);
refreshChangedPanels(result.diff);
render(result.doc.tree);如果你手上没有 newSource,先拼一下:
const previous = session.getDocument();
const newSource =
previous.source.slice(0, startOffset) +
newText +
previous.source.slice(oldEndOffset);从 1.2.3 开始,low-level updater 不再公开导出,公共接入请用 session API。
整套 API 只有两个角色:
吃进一整篇 source,跑一遍 structural parse,返回一个快照对象 doc(含 source / tree / zones)。
它不负责后续编辑,只建立增量系统的初始状态。大多数情况下你不需要直接调它——createIncrementalSession
内部会帮你做这件事。只有你明确想手动持有初始快照对象时才需要。
返回一个基于闭包的状态容器,内部维护着一份 currentDoc(最新的 source / tree / zones),对外暴露四个方法:
| 方法 | 作用 |
|---|---|
getDocument() |
读取当前快照(不推进状态) |
applyEdit(edit, newSource, options?) |
推进到下一版,自动选择增量或全量 |
applyEditWithDiff(edit, newSource, options?, diffOptions?) |
同上,但额外返回一份结构化 diff(支持按次覆盖 diff 细化预算) |
rebuild(newSource, options?) |
放弃增量,直接全量重建 |
applyEdit 和 applyEditWithDiff 都会真正推进 session 状态。后者不是"只看看 diff"的观察接口。如果你想试探 diff
而不影响真实状态,在副本 session 上操作。
"我在接增量解析"
│
┌─────────────┼──────────────┐
▼ ▼ ▼
首次打开文档? 正常编辑? 不信任缓存 /
│ 配置大幅变更?
│ ┌─────┴──────┐ │
▼ ▼ ▼ ▼
createIncremental 需要知道 不需要 session
Session(...) 哪里变了? diff .rebuild(...)
│ │
▼ ▼
applyEdit applyEdit
WithDiff (...)
(...)
| 你想做的 | 用这个 |
|---|---|
| 首次加载,建立会话 | createIncrementalSession(...) |
| 每次编辑,推进缓存 | session.applyEdit(...) |
| 推进缓存 + 拿结构 diff | session.applyEditWithDiff(...) |
| 强制全量重建 | session.rebuild(...) |
| 读当前快照(不推进) | session.getDocument() |
| 手动持有初始快照(low-level) | parseIncremental(...) |
-
edit告诉 session 旧文档里哪一段被替换了——用于定位脏区、走增量。 -
newSource告诉 session 替换后整篇文档长什么样——用于校验一致性、保证结果正确。
两者缺一不可:只有 newSource 就不知道哪里变了;只有 edit 就无法确认最终结果。
import {
createIncrementalSession,
parseIncremental,
} from "yume-dsl-rich-text";parseIncremental(
source
:
string,
options ? : IncrementalParseOptions,
):
IncrementalDocument
createIncrementalSession(
source
:
string,
options ? : IncrementalParseOptions,
sessionOptions ? : IncrementalSessionOptions,
):
{
getDocument: () => IncrementalDocument;
applyEdit: (
edit: IncrementalEdit,
newSource: string,
options?: IncrementalParseOptions,
) => IncrementalSessionApplyResult;
applyEditWithDiff: (
edit: IncrementalEdit,
newSource: string,
options?: IncrementalParseOptions,
diffOptions?: IncrementalDiffRefinementOptions,
) => IncrementalSessionApplyWithDiffResult;
rebuild: (newSource: string, options?: IncrementalParseOptions) => IncrementalDocument;
}| API | 推荐程度 | 干嘛的 |
|---|---|---|
createIncrementalSession(source, options?, sessionOptions?) |
默认推荐 | 会话级安全入口(applyEdit 自动回退 + 自适应策略) |
parseIncremental(source, options?) |
可选(low-level) | 手动构建初始快照 |
interface IncrementalSessionApplyResult {
doc: IncrementalDocument;
mode: "incremental" | "full-fallback";
fallbackReason?:
| "INVALID_EDIT_RANGE"
| "NEW_SOURCE_LENGTH_MISMATCH"
| "EDIT_TEXT_MISMATCH"
| "UNKNOWN"
| "INTERNAL_FULL_REBUILD"
| "FULL_ONLY_STRATEGY"
| "AUTO_COOLDOWN"
| "AUTO_LARGE_EDIT";
}
interface IncrementalSessionApplyWithDiffResult extends IncrementalSessionApplyResult {
diff: TokenDiffResult;
}
interface TokenDiffResult {
isNoop: boolean;
patches: Array<{
kind: "insert" | "remove" | "replace";
oldRange: { start: number; end: number };
newRange: { start: number; end: number };
}>;
unchangedRanges: Array<{
oldRange: { start: number; end: number };
newRange: { start: number; end: number };
}>;
ops: Array<
| {
kind: "splice";
path: Array<{ field: "root" | "children" | "args"; index: number }>;
field: "root" | "children" | "args";
oldRange: { start: number; end: number };
newRange: { start: number; end: number };
oldNodes: StructuralNode[];
newNodes: StructuralNode[];
}
| {
kind: "set-text" | "set-escape" | "set-raw-content";
path: Array<{ field: "root" | "children" | "args"; index: number }>;
oldValue: string;
newValue: string;
}
| {
kind: "set-implicit-inline-shorthand";
path: Array<{ field: "root" | "children" | "args"; index: number }>;
oldValue?: boolean;
newValue?: boolean;
}
>;
dirtySpanOld: { startOffset: number; endOffset: number };
dirtySpanNew: { startOffset: number; endOffset: number };
}diff 各字段:
-
isNoop:当这次编辑没有产生结构 token 变化时为true(patches和ops都为空)。 -
patches:顶层 token-index patch 列表(可多段)。 -
unchangedRanges:顶层稳定岛范围。 -
ops:路径感知的结构化操作(如 nestedchildren/argssplice、set-text、set-raw-content)。 -
dirtySpanOld/dirtySpanNew:旧/新源码坐标系下的脏区间。
如果内部无法完成精确 diff 细化,applyEditWithDiff(...) 会保守退回成整树替换型 diff,不会中断 session 更新。
另外,对于已经走到 full-fallback 的超大文档,applyEditWithDiff(...) 可能会直接选择保守整树 diff,以避免病态深度细化开销。
| 策略 | 行为 |
|---|---|
"auto"(默认) |
大编辑直接全量;可能进入临时全量优先冷却窗口;非冷却期优先增量 |
"incremental-only" |
优先走增量,仅在底层更新失败时回退全量 |
"full-only" |
始终全量重建,fallbackReason 固定为 FULL_ONLY_STRATEGY
|
策略在 createIncrementalSession 的第三个参数 sessionOptions 中设置:
// 始终全量(调试 / 基线对比)
const session = createIncrementalSession(source, {handlers}, {
strategy: "full-only",
});
// 始终优先增量,只在底层失败时回退
const session = createIncrementalSession(source, {handlers}, {
strategy: "incremental-only",
});sessionOptions 的所有字段都是可选的,未传的字段使用默认值。只想调整 auto 策略中的个别参数时,只传你要覆盖的那几个就行,其余保持默认:
// auto 策略(默认),但把大编辑阈值放宽到 40%
const session = createIncrementalSession(source, {handlers}, {
maxEditRatioForIncremental: 0.4,
});
// auto 策略,缩短冷却窗口
const session = createIncrementalSession(source, {handlers}, {
fullPreferenceCooldownEdits: 6,
});
// 同时调多个
const session = createIncrementalSession(source, {handlers}, {
maxEditRatioForIncremental: 0.4,
fullPreferenceCooldownEdits: 6,
softZoneNodeCap: 128,
diff: {
refinementDepthCap: 64,
maxComparedNodes: 20000,
maxAnchorCandidates: 128,
maxOps: 512,
maxSubtreeNodes: 256,
maxMilliseconds: 8,
},
});
// 热路径可按次覆盖(不改 session 默认值)
const withDiff = session.applyEditWithDiff(
edit,
newSource,
undefined,
{maxMilliseconds: 4, maxOps: 256},
);| 选项 | 默认值 | 作用 |
|---|---|---|
maxEditRatioForIncremental |
0.2 |
若 max(替换长度, 插入长度) / 旧文档长度 超过阈值,直接全量 |
sampleWindowSize |
24 |
自适应统计的滑动窗口大小 |
minSamplesForAdaptation |
6 |
启动自适应判断所需最小样本数 |
maxFallbackRate |
0.35 |
窗口内回退率超过阈值时,切入全量优先冷却 |
switchToFullMultiplier |
1.1 |
若增量平均耗时 > 全量平均耗时 × 倍率,切入全量优先冷却 |
fullPreferenceCooldownEdits |
12 |
冷却窗口编辑次数 |
softZoneNodeCap |
64 |
纯 inline / 低 breaker 文档的软 zone 切分上限;越小 zone 越细,越大脏窗越宽 |
diff |
undefined |
session 级默认 diff 细化配置;不传时使用内部默认预算 |
-
refinementDepthCap控制applyEditWithDiff(...)继续做路径感知深层细化的最大深度。 - 值越大,深树场景下保留的细粒度
ops越多,但极端深度下开销也更高。 - 值越小,会更早切到粗粒度 splice,通常更有利于最坏延迟。
- 非法值会被归一化为默认值。
-
maxComparedNodes/maxAnchorCandidates/maxOps/maxSubtreeNodes/maxMilliseconds共同构成默认 diff 硬预算:目标是“常态尽量细,超限立刻粗化”,而不是在任何输入上继续追求最小 patch。 - 这些预算在内部由一个显式的
DiffBudgetState累积和传播;预算超限属于预期内退化条件,不走 throw/catch 异常流,而是直接切到 coarse splice / conservative diff。 - 当 session 这次编辑实际走的是增量路径时,
applyEditWithDiff(...)会先复用增量 dirty zone 的源区间,只在这段 root window 内做精细 diff;dirty window 外直接按稳定区处理,不再对整棵 root tree 做细化。
| Diff 选项 | 默认值 | 作用 |
|---|---|---|
refinementDepthCap |
64 |
applyEditWithDiff 的嵌套结构 diff 细化最大深度,超过后降级为较粗粒度 splice |
maxComparedNodes |
20000 |
单次 diff refinement 允许的节点比较总预算;超限后立刻退化到更粗粒度 diff |
maxAnchorCandidates |
128 |
单次 diff refinement 允许尝试的唯一锚点候选上限;超限后放弃锚点细化 |
maxOps |
512 |
单次 diff refinement 允许产出的 structural diff op 上限;超限后回退到 root splice 级别 |
maxSubtreeNodes |
256 |
仍允许做嵌套细化的子树规模上限;过大的子树直接 coarse splice |
maxMilliseconds |
8 |
单次 diff refinement 的软 wall-clock 预算;超时后退化到更粗粒度 diff |
底层校验/一致性失败:
-
INVALID_EDIT_RANGE:offsets 对旧文本非法(负数、反向、越界)。 -
NEW_SOURCE_LENGTH_MISMATCH:newSource.length和 edit delta 对不上。 -
EDIT_TEXT_MISMATCH:edit.newText和newSource在startOffset处的切片不一致。 -
UNKNOWN:非预期异常(多半是实现 bug 或外部异常)。
策略触发的回退:
-
AUTO_LARGE_EDIT:auto 策略觉得这次编辑改的太多了。 -
AUTO_COOLDOWN:auto 策略暂时进入全量偏好的冷却窗口。 -
FULL_ONLY_STRATEGY:你自己设了strategy: "full-only"。 -
INTERNAL_FULL_REBUILD:增量保护路径内部发现不对劲,主动升级成全量重建。
上半部分是 session 层的策略门控,下半部分是内部增量更新的实际工作流。
flowchart TD
A["applyEdit(edit, newSource)"] --> B{strategy?}
B -->|full - only| C["全量重建"]
C --> C1["✗ full-fallback · FULL_ONLY_STRATEGY"]
B -->|auto / incremental - only| D{"auto 且<br/>editRatio > 阈值?"}
D -->|是| E["全量重建"]
E --> E1["✗ full-fallback · AUTO_LARGE_EDIT"]
D -->|否| F{"auto 且<br/>处于冷却窗口?"}
F -->|是| G["全量重建"]
G --> G1["✗ full-fallback · AUTO_COOLDOWN"]
F -->|否| H["进入内部增量路径 ↓"]
H --> V{"edit 范围<br/>校验通过?"}
V -->|否| V1["✗ full-fallback · 校验错误码"]
V -->|是| FP{"options 指纹<br/>兼容?"}
FP -->|否| FP1["✗ full-fallback · INTERNAL_FULL_REBUILD"]
FP -->|是| ZE{"zone 列表<br/>非空?"}
ZE -->|否| ZE1["✗ full-fallback · INTERNAL_FULL_REBUILD"]
ZE -->|是| ZL{"zone 数量<br/>> 1?"}
ZL -->|否| ZL1["✗ full-fallback · INTERNAL_FULL_REBUILD<br/>(低 zone 守卫)"]
ZL -->|是| TG{"尾部覆盖<br/>安全?"}
TG -->|否| TG1["✗ full-fallback · INTERNAL_FULL_REBUILD"]
TG -->|是| DZ["定位脏区 zone 范围"]
DZ --> RP["重解析脏窗口<br/>+ 右边界稳定扩展"]
RP --> BG{"扩展字节<br/>超预算?"}
BG -->|是| BG1["✗ full-fallback · INTERNAL_FULL_REBUILD"]
BG -->|否| RZ{"有右侧<br/>zone 待复用?"}
RZ -->|否| AS
RZ -->|是| SP{"右接缝<br/>seam probe<br/>通过?"}
SP -->|否| SP1["✗ full-fallback · INTERNAL_FULL_REBUILD"]
SP -->|是| AS["拼接:左侧原样 +<br/>重解析结果 +<br/>右侧惰性 delta 平移"]
AS --> OK["✓ incremental"]
session.applyEdit(...) 不抛异常——总是返回结构化结果对象。
const result = session.applyEdit(edit, newSource);
if (result.mode === "full-fallback") {
console.log(result.fallbackReason);
}增量接口设计上是一次调用对应一个 edit。如果编辑器一次提交了多段 patch,常见做法:
- 按编辑器的应用顺序逐个调用
session.applyEdit(...),每步传对应的newSource。(推荐——出了问题定位容易) - 在编辑器层把多段 patch 合并成一个大的
(startOffset, oldEndOffset, newText),然后只调一次。
如果你的 UI 会缓存 diff 结果用于展示,尽量避免"按帧合并后只保留最后一条"的覆盖策略。同一帧里如果连发多次编辑,后一次
isNoop: true 可能把前一次有意义的 diff 覆盖掉。
最典型的增量管线——"结构缓存负责定位,token-walker 负责局部 token 化":
- 用
session.applyEdit(...)维护最新的doc.tree/doc.zones。 - 用结构查询(比如
nodeAtOffset/enclosingNode)找到应该重算 tokens 的范围。 - 调
parseSlice(fullText, node.position, dsl, tracker)只把那个范围重新解析成TextToken[]。
能跑通的关键是:doc.tree 的 position 一定回指整篇 newSource(增量内部强制开了 trackPositions)。
从 1.2.4 开始,右侧 zone 复用改成了惰性 delta 平移:每个 zone 只做 O(1) 的偏移量累积,节点位置要等到你真正去读
doc.tree 或 doc.zones[i].nodes 时才物化。连续多次头部编辑的 delta 会自动叠加,不触发任何中间物化。
- 头部编辑不再是性能黑洞。 以前每次编辑都要深拷贝整个右侧子树,现在只记一个数字。
- 中部/尾部编辑和以前一样快——脏区本来就很小。
- 唯一要"付账"的时刻是你实际遍历
doc.tree做渲染/导出时。
增量解析的收益取决于 zone 数量——zone 越多,单次编辑需要重解析的窗口越小。
问题: 在 1.2.4 之前,buildZones(...) 只在 raw / block 节点处切分。纯 inline 文档只会产出 1 个 zone,等于跟全量一样。
解决方案: 1.2.4 的增量路径内部引入了 softZoneNodeCap(默认 64)——连续非 breaker 节点超过此数量时自动切分。切分点始终落在节点边界上,不影响
seam probe 不变量。公开 API buildZones(...) 不受影响。
const session = createIncrementalSession(source, {handlers}, {
softZoneNodeCap: 128, // 更大的 zone → 更少的签名比对开销,但脏窗更宽
});当上一快照的 zone 数量 ≤ 1 时,增量路径直接跳到全量重建——1 个 zone 没有任何左/右可复用结构,增量路径的全部开销都是浪费。触发时返回
INTERNAL_FULL_REBUILD。
| 场景 | zone 数 | 增量 median | 全量 | 加速比 | GC 稳定性 |
|---|---|---|---|---|---|
| 纯 inline(softCap=64) | 264 | ~12 ms | ~127 ms | ~10× | 50 次连续编辑无退化,median ~9 ms |
| 稀疏 raw(每 50 行 1 个 breaker) | 1571 | ~12 ms | ~130 ms | ~10× | — |
| 中等 raw(每 20 行 1 个 breaker) | 3757 | ~15 ms | ~130 ms | ~9× | — |
| 密集 raw(每 10 行 1 个 breaker) | 7015 | ~19 ms | ~130 ms | ~7× | — |
| 超密集 raw(每 3 行 1 个 breaker) | 17871 | ~38 ms | ~136 ms | ~3.5× | — |
测试环境与性能页同机同口径。编辑位置固定在头部区域(最坏方向),单字符替换。
zone 不是越多越好——每个 zone 都有签名计算、assembly 拼接、delta shift 的固定常数开销。几百到几千 zone 是甜区,超过万级后 zone 开销反而占主导。
经验法则:只要文档长到全量重建开始让你感觉到延迟(比如 > 10 ms),zone 切分就能把增量拉到可用范围。
flowchart TD
N["顶层节点序列"] --> IS{"节点是<br/>raw / block?"}
IS -->|是| FH["flush 已累积软 zone"]
FH --> HB["硬边界:独占 zone"]
HB --> IS
IS -->|否| ACC["累积到当前软 zone"]
ACC --> CAP{"累积节点数<br/>≥ softZoneNodeCap?"}
CAP -->|是| FS["flush 当前软 zone"]
FS --> IS
CAP -->|否| IS
- 文档很大且编辑频率高:用 session,让惰性平移帮你攒着,等到渲染帧再去读
doc.tree。 - 文档很大但只需要局部 token:组合
parseSlice(...)收益最大。 - 把 options 当作不可变配置:
parseIncremental会把 options 做快照存进doc.parseOptions(handlers 里 plain object/array 字段会递归克隆)。 - 保持
handlers引用稳定:handler 函数引用不变 = fingerprint 不变 = 不会触发全量。
-
oldEndOffset是 exclusive 的,不是 inclusive。off-by-one 的 bug 出了挺难查的。 - offsets 都是 JavaScript 字符串 offset(UTF-16 code units),不是字符数 / rune count。碰到 emoji 或 CJK 扩展字符时这个区别就出来了。
-
edit.newText必须和newSource对得上。传了长度对但内容不对的进去,会直接被EDIT_TEXT_MISMATCH拦住。 - raw/block 语法依赖真实换行:如果你在测试里写
"\\n"(反斜杠+n)而不是"\n",解析行为会完全不一样。
常规编辑器接入请直接用 createIncrementalSession(...)。
const doc = parseIncremental(source, options);doc 的保证:
-
doc.source和你传入的source完全一致。 -
doc.tree来自 structural parse,内部强制开了trackPositions: true。 -
doc.zones是buildZones(doc.tree)的结果。 - zone 用到的节点都带着源码位置(offset/line/column),坐标系基于
doc.source。
options 的行为:
-
trackPositions会被忽略/覆盖为true。 -
options会被存成内部快照(handlers里的 plain object/array 字段会做递归快照)。 - 后续增量复用会比较内部的 options fingerprint(看的是 parser 的有效行为:syntax/forms/handler 函数身份)。
- 每次传新的
options对象,只要行为等价,就不会触发全量回退。
增量更新按 Zone[] 做边界稳定,不是随便选个窗口重算。理解下面这些规则能帮你弄清楚为什么某些情况下它会回退全量。
-
初始脏区(zone 级):
-
findDirtyRange用单遍线性扫描同时完成 overlap 检测和插入点定位(1.2.5 起合并为一次遍历)。 - 如果 edit 和已有 zone 有交集:初始脏区范围是"相交区间 + 左边 1 个 zone + 右边 1 个 zone"。多扩一个 zone 是为了确保编辑边界处的上下文不丢失。
- 如果 edit 是纯插入且没有和任何 zone 相交:初始范围取插入点两侧的相邻 zone。
-
-
右边界稳定扩展:
- 重解析当前脏区之后,如果重解析末端 offset 和当前脏区右端 offset 不一致,说明结构还没稳定,继续向右扩 1 个 zone。一直扩到右边界稳定或到了文档末尾。
-
右接缝 seam probe 复用校验:
- 右侧 zone 要复用,必须先过 seam probe:在拼接边界重新解析一小段,比较产出的 zone 签名和旧的右侧 zone 签名。
- 签名用有界采样哈希(头尾各 32 字符),O(1) 完成单节点比对。
- probe 没过就直接回退全量——宁可慢一次也不要拼出错误的树。
-
扩展预算保护:
- 右扩过程中有累计重解析字节预算(
2 × newSource.length)。超过阈值就自动回退全量,防止极端情况下反复扩窗把性能打穿。
- 右扩过程中有累计重解析字节预算(
-
右侧惰性 delta 平移(1.2.4+):
- 通过 seam probe 的右侧 zone 只记
pendingDelta,节点位置到消费时才物化。连续编辑的 delta 自动叠加。
- 通过 seam probe 的右侧 zone 只记
-
快照异常回退:
- 输入快照本身有问题(zones 为空、尾部 coverage 异常),直接回退全量重建。
-
全量回退路径的常数优化(1.2.5+):
-
cloneParseOptions延后到增量路径确认后才执行。 - 增量路径已构建的
positionTracker在 fallback 全量重建时直接复用。
-
增量 API 的版本差异、升级影响,以及像 1.4.1 这类 EOF 未闭合 inline 恢复语义变化,现在统一收在:
export interface IncrementalEdit {
startOffset: number;
oldEndOffset: number; // exclusive,基于旧文本 offsets
newText: string;
}startOffset / oldEndOffset 基于旧文本(session.getDocument().source)。newText 必须与
newSource.slice(startOffset, startOffset + newText.length) 一致。
export interface IncrementalDocument {
source: string;
zones: readonly Zone[];
tree: readonly StructuralNode[];
parseOptions?: IncrementalParseOptions;
}export type IncrementalParseOptions = Omit<
StructuralParseOptions,
"trackPositions" | "baseOffset" | "tracker"
>;它和 StructuralParseOptions 几乎一样,只是去掉了增量内部自己管理的三个字段。实际可用的字段:
| 字段 | 来源 | 说明 |
|---|---|---|
handlers |
ParserBaseOptions |
标签处理器映射(保持引用稳定可提升增量命中率) |
allowForms |
ParserBaseOptions |
允许的标签形式(inline / raw / block) |
implicitInlineShorthand |
ParserBaseOptions |
隐式 inline 简写开关 |
depthLimit |
ParserBaseOptions |
最大嵌套深度(默认 50) |
syntax |
ParserBaseOptions |
自定义 DSL 语法标记 |
tagName |
ParserBaseOptions |
自定义标签名字符规则 |
trackPositions |
被 Omit | 增量路径内部强制开启,不可覆盖 |
baseOffset |
被 Omit | 增量切片更新的内部参数 |
tracker |
被 Omit | 增量切片更新的内部参数 |
也就是说,如果你已经会用 parseStructural 的 options,那 createIncrementalSession 的 options 参数直接把同一个对象传进去就行——被
Omit 的字段传了也会被忽略(TypeScript 类型层面会报错,但语义上不影响)。
export interface IncrementalSessionOptions {
strategy?: "auto" | "incremental-only" | "full-only";
sampleWindowSize?: number;
minSamplesForAdaptation?: number;
maxFallbackRate?: number;
switchToFullMultiplier?: number;
fullPreferenceCooldownEdits?: number;
maxEditRatioForIncremental?: number;
softZoneNodeCap?: number;
diff?: IncrementalDiffRefinementOptions;
}
export interface IncrementalDiffRefinementOptions {
refinementDepthCap?: number;
maxComparedNodes?: number;
maxAnchorCandidates?: number;
maxOps?: number;
maxSubtreeNodes?: number;
maxMilliseconds?: number;
}softZoneNodeCap 控制增量内部 zone 构建的软切分上限,默认 64。最小有效值 2(内部 clamp)。
增量类型的公开导出面刻意保持小而稳。diff 的细粒度片段类型故意不从 root 单独导出——稳定主契约是 TokenDiffResult
,需要更细类型时从字段派生:
type DiffPatch = TokenDiffResult["patches"][number];
type DiffRange = TokenDiffResult["unchangedRanges"][number];
type DiffOp = TokenDiffResult["ops"][number];import {
createIncrementalSession,
parseIncremental,
} from "yume-dsl-rich-text";
import type {
IncrementalDiffRefinementOptions,
IncrementalDocument,
IncrementalEdit,
IncrementalParseOptions,
IncrementalSessionApplyResult,
IncrementalSessionApplyWithDiffResult,
IncrementalSessionOptions,
TokenDiffResult,
} from "yume-dsl-rich-text";