Add canonical query semantics, managed metadata and transactions, overlay-aware lookup, reproducible benchmarks, and synchronized API documentation.
12 KiB
tp API 语义规范
本文档是 tp 核心 API 的当前行为契约。代码、测试、README 与 docstring 若与本文冲突,应以经过测试的代码为准并同步修正文档。
tp 当前定位是 Emacs 文本属性的高层操作工具箱,以及一套受管理的命名层、层栈和响应式渲染模型。它尚不是全部原生文本/字符属性语义的等价替代品;完整范围与路线见 REPOSITORY-AUDIT.md。
Stage 2 canonical façade 已完成为内部模型:tp--native-range、tp--presence、tp--request、tp--match、tp--result 是模块间传递的规范记录。公开入口和历史返回值保持兼容,不因内部模型收敛而改变。
Stage 3 text-only 原生语义 façade 已完成:direct/effective/source-aware lookup、property change/any/not-all 与三种 mutation policy 组合均有明确公开边界。
Stage 4 managed lifecycle 已完成:managed metadata、attach/detach、diagnostics、transaction、参数化 mounted layer args/version refresh 与 theme generation diagnostics 均有公开边界。
Stage 5 overlay-aware 字符属性查询已完成:tp-lookup :char / :char-source 报告 Emacs 选出的字符属性值、overlay 来源和获胜 overlay 身份。overlay 创建、移动、删除、priority 管理和生命周期不属于 tp 契约。
1. 对象、范围与坐标
- 所有范围均采用半开区间
[START, END)。 - 字符串使用 Emacs 原生的 0-based 坐标,合法边界为
0到(length STRING)。 - 缓冲区使用 Emacs 原生位置,通常从
point-min开始;显式 BUFFER 和 nil(当前缓冲区)具有相同语义。 - 范围结果原则上使用目标对象的原生坐标。当前公开兼容例外是
tp-intervals/tp-intervals-map:缓冲区默认返回相对 START 的 offset,传入ABSOLUTE才返回可直接回传给写 API 的原生坐标。内部 canonical range 使用原生坐标,但该 relative 默认作为历史兼容例外保留。
2. 修改策略
| 调用族 | 字符串 | 缓冲区 |
|---|---|---|
tp-set / tp-reset / tp-add / tp-remove 整串形式 |
复制后返回新字符串 | 不适用 |
| 上述函数的 START/END 形式 | 原地修改显式字符串 OBJECT | 原地修改 |
tp-match-* / tp-regexp-* |
返回新字符串 | 原地修改,返回匹配范围 |
| 层栈修改函数 | 原地修改字符串 | 原地修改 |
tp-forward-do / tp-backward-do / tp-search-map |
原地修改;替换文本必须等长 | 原地修改;替换文本可增减长度 |
普通缓冲区写入使用 Emacs 的属性修改原语,因此遵循 Emacs 的 modified、undo 和 read-only 行为;tp 不把所有修改默认包装为 silent modification。响应式重渲染和 tp-text 文本替换为了恢复受管理输出会在内部允许修改 read-only 文本,这是当前实现边界,不代表公开 API 已提供统一的 read-only 策略。相同值刷新会尽量避免无意义地翻转 buffer-modified 状态。
3. presence、nil 与 wildcard
以下三种状态必须区分:
- 属性键不存在;
- 属性键存在,值为 nil;
- 属性键存在,值为非 nil。
tp-member 用于判断直接属性键是否存在;tp-at / plist-get 单独使用时不能区分前两种状态。删除最后一个子属性会移除空的父属性键,不会偶然留下 (PROPERTY nil)。
搜索 API 的规则是:
- 省略 VALUE:匹配该直接属性的任意已存在值;
- 显式传入 nil:只匹配“键存在且值为 nil”;
- 需要继续提供 OBJECT、次数或范围等后续位置参数时,传入公共唯一哨兵
tp-any-value表示任意值; - 自定义 PREDICATE 总是优先执行,并接收请求 VALUE(可能是
tp-any-value)和实际属性值。
属性缺失的范围不属于搜索结果,即使搜索值是 nil。
4. 搜索结果
tp-search对字符串和缓冲区范围都返回(START END VALUE)列表。tp-forward/tp-backward的字符串路径返回前/后 N 个(START END VALUE);缓冲区路径移动 point,并返回第 N 次搜索的prop-match。tp-forward-do/tp-backward-do只在第 N 个匹配上调用函数;匹配不足 N 个时不调用函数,返回实际找到的数量。tp-search-map处理全部匹配并返回处理数量。
tp-forward / tp-backward 的对象相关结果差异是现存公开兼容契约。内部搜索路径已收敛到 canonical match/result 记录;公开返回结构暂不改变。
5. 原生文本查询与修改策略
Stage 3/5 查询 façade 已完成:text modes 保持 text-only,char modes 委托 Emacs 的 overlay-aware 字符属性查询。
tp-lookup 返回 tp-lookup-result 记录,字段为 property、value、present-p、source、mode、object、position、overlay。text modes 中 overlay 始终为 nil;:char / :char-source 中 overlay 为 Emacs 选出的获胜 overlay,若文本 fallback 获胜则为 nil。
| MODE | 语义 |
|---|---|
:text-direct |
只读取直接文本属性;显式 nil 与缺失通过 present-p/source 区分 |
:text-effective |
值使用 get-text-property;source 使用 text-only 来源解释 |
:text-source |
返回 direct/category/alias/default/absent 来源和值,不查看 overlay |
:char |
使用 get-char-property-and-overlay 的值,按 Emacs overlay/text 优先级解析 |
:char-source |
同 :char,并在 overlay 获胜时把 source 设为 :overlay、overlay 设为获胜 overlay |
source 取值为 :text-direct、:category、:alias、:default、:overlay 或 :absent。direct 显式 nil 的结果是 present-p 为 t、value 为 nil、source 为 :text-direct;alias nil 与 Emacs 原生语义一致,会继续寻找后续 alias/default;缺失属性的结果是 present-p 为 nil、value 为 nil、source 为 :absent。
tp-property-change 是 Emacs property change 原语的显式封装::direction :next / :previous 选择 next/previous,传入 :property 时使用 single-property change,省略时使用 all-property change。
tp-property-any / tp-property-not-all 是 text-property-any / text-property-not-all 的薄封装,保留 Emacs 对显式 nil、边界和对象的行为。
tp-with-mutation-policy 只接受三种有效组合:
| POLICY | 语义 |
|---|---|
(:modified :ordinary :read-only :respect) |
普通修改,尊重 read-only |
(:modified :ordinary :read-only :inhibit) |
绑定 inhibit-read-only,普通 modified/undo 行为 |
(:modified :silent :read-only :inhibit) |
绑定 inhibit-read-only 并使用 with-silent-modifications |
(:modified :silent :read-only :respect) 明确拒绝,因为 silent modification 与尊重 read-only 不能同时满足。
insert、copy、yank、stickiness、narrowing 和 indirect buffer 行为直接委托 Emacs;tp 不为这些原生操作提供 wrapper。
6. 直接模板展开与 managed mount
层名有两种不同用途:
6.1 直接模板展开
把非响应式层名传给 tp-set、tp-reset、tp-add、tp-match-* 或 tp-regexp-* 时,层定义展开为普通属性,通常不保留 tp-name。结果不能依赖层名进行后续移动、隐藏、按名删除或静态重定义刷新。
匿名响应式属性和响应式层需要保留 tp-name 才能登记与刷新,这是直接路径中的受管理例外。
6.2 managed mount
tp-push-layer / tp-put-layer 明确保留 tp-name 和必要的 tp-layers 状态。mounted layer 可以被查询、移动、隐藏、显示、删除和响应式刷新。
非参数化层重新定义后,已挂载区域按 old/new 所有权协调:
- 新定义写入其拥有的键;
- 旧定义拥有、但新定义不再拥有的键,仅在当前值仍等于旧值时移除;
- 外部已经改写的值不会被当作旧层残留删除。
所有 managed mount 都携带 lifecycle metadata。即使只有单个 managed layer,只要存在 tp-meta,权威存储也使用 tp-layers;直接渲染属性和 tp-layer-stack-at 等 public stack query 不暴露 tp-meta。
参数化层的已挂载 entry 保存调用实参、形参表和 definition version;重新定义参数化层后,既有 managed entry 会按保存的 args 刷新。历史无 metadata 的 entry 按 legacy entry 保守处理。
tp-attach-managed-layers 扫描已经进入缓冲区的 managed storage,补齐/规范化 metadata,登记发现的层,并返回层名列表。tp-detach-managed-layers 移除 managed storage;KEEP-RENDERED 非 nil 时保留当前可见渲染属性为普通文本属性。
tp-managed-layer-diagnostics、tp-managed-buffer-diagnostics 和 tp-managed-diagnostics 是只读诊断入口,报告 entries、args、registry、errors 与 theme diagnostics。tp-managed-diagnostics 的 theme 部分报告 generation、last hook source、refresh mode、refreshed ranges 和 errors;这是 lifecycle 诊断,不是性能基准。
tp-layer-transaction 在给定范围内执行 managed stack 修改。成功返回结构化 plist,其中 :status 为 ok、:ok 为 t,:result 保留 FUNCTION 的返回值;失败时恢复事务前文本/属性快照并默认发出 tp-layer-transaction-error,NOERROR 非 nil 时返回结构化失败 plist。
7. tp-text 替换
- 初次应用和响应式更新都按内嵌字符串的真实属性 interval 处理,不从位置 0 采样后扩散到整段。
- 调用者显式属性覆盖内嵌属性;显式 nil 也是有效覆盖值。
- 未被调用者覆盖的内嵌属性按各自 interval 保留。
- 每个 interval 算出待写属性后,
tp-set只覆盖这些键并保留其他目标属性,tp-reset替换目标的完整属性集合,tp-add则对目标已有的 face-family 与嵌套 plist 继续合并;文本内容相同和发生替换时遵守同一规则。 - 字符串和缓冲区路径遵守相同的 per-interval 属性计算;对象的复制/原地策略仍按第 2 节执行。
8. 错误边界
- 未定义或无法解析的层使用
tp-unresolved-layer表达。 - 栈 API 的 NOERROR 只抑制
tp-unresolved-layer;参数化层 body、计算、属性结构和其他内部错误必须传播。 - 隐藏层存储发生所有权冲突时使用
tp-layer-conflict。 - transform 失败或返回非字符串、compute 失败都属于业务输出失败,必须传播。
- watcher 属于 observer:单个 watcher 失败不会阻断 managed update;失败会记录到
tp-reactive-observer-errors(newest first)并输出消息。 - 公开边界不应把内部失败转换为默认值后继续写入。
9. 隐藏层与外部直接修改
存在隐藏层时,tp-layers 保存完整 managed stack,其他直接属性是第一个可见层的渲染缓存。两类调用采用不同但明确的策略:
- definition/reactive refresh 知道正在刷新哪个定义,也知道直接属性对应哪个可见 entry;它先把原生直接编辑协调进该可见 entry,再执行 old/new 所有权刷新,因此外部改写值不会被当作旧定义残留删除;
- 普通 stack decode/write 缺少这次 definition refresh 的所有权上下文,缓存不一致时会在写入前发出
tp-layer-conflict,且不修改层栈; - 所有层都隐藏时没有可接收直接属性的可见 entry,此时出现直接属性始终发出
tp-layer-conflict。
tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。
10. 返回值现状
核心写 API 的返回值仍保留历史差异:
- buffer/region
tp-set/tp-reset/tp-add:(START . END); - 整串复制式写入:新字符串;
- buffer
tp-remove/tp-clear:nil; - stack mutator:修改的 property-run 数量;
tp-put-layer/tp-push-layer:显式 OBJECT 或(START . END)。
这些返回值是当前兼容契约。property-run 数量会受无关 interval 边界影响,不应被当成稳定业务标识。内部 request/result 模型已建立,但不会改变这些历史 public returns。
11. 明确不在当前完整契约内
以下能力仍需设计或补齐,不能据现有 API 推断:
- overlay lifecycle(创建、移动、删除、priority 管理);
- insert/copy/yank/stickiness 的 tp wrapper 或 managed workflow;
- mutation policy 三种组合之外的统一 read-only、silent modification、undo 策略;
- 字符串/缓冲区完全一致的搜索结果结构;
- observer 错误的清理、重试与汇总策略。