13 KiB
TP
TP 1.0 是一个可独立使用的 Emacs retained/reactive text runtime。它把声明式属性、响应式数据和稳定文本对象投影到 string 与 buffer,并拥有文本属性 contribution 合成、精确依赖追踪、保留式身份、marker-backed mount、diff、事务、回滚和最终 Buffer publication。
TP 不依赖 Ebox 或 ECSS,也不实现 CSS selector、stylesheet、specificity、cascade winner、Box、Flex、Grid、测量或布局。需要 CSS 的调用者可以先用 ECSS 算出最终 declarations,再交给 TP 发布;TP 自身只理解 Emacs 文本属性和通用 retained text surface。
英文文档:README.md。
完整公共 API 参考:API-REFERENCE.md。本文负责概念 和快速开始;API 参考按当前 TP 1.0 源码列出入口、参数语义、返回值和用法。
运行要求
- Emacs 28.1 或更高版本。
- 没有第三方运行时依赖。
(add-to-list 'load-path "/path/to/tp")
(require 'tp)
选择最小的公共入口
| 需求 | API | 是否建立 live runtime |
|---|---|---|
| 返回带属性的字符串 | tp-propertize |
否 |
| 一次性装饰已有 buffer 范围 | tp-apply |
否 |
| 响应式装饰 host-owned 文本 | tp-watch |
是,properties capability |
| 管理 retained text content | tp-surface-mount / tp-surface-update |
是,content capability |
| 检查或卸载 retained publication | tp-surface-report / tp-surface-inspect / tp-surface-unmount |
是 |
一次性 API 和 retained API 使用相同的 property policy 与 projection 语义。一次性调用不会创建 object、binding、marker、subscription 或 surface state。
静态文本属性
(let* ((callback (lambda (_window _object _position) "Open"))
(text
(tp-propertize
"Hello"
(list 'face '(:foreground "white" :background "navy")
'help-echo callback
'keymap nil))))
text)
显式 nil 和属性不存在是两种状态。上例中的 keymap 存在且值为 nil。函数对象始终是 literal data,因此 TP 会保留 callback,不会调用它。
一次性修改已有范围而不改变文字:
(with-current-buffer (get-buffer-create "*tp-demo*")
(erase-buffer)
(insert "abcdef")
(tp-apply (current-buffer) 2 5 '(face italic)))
原有的 tp-set、tp-reset、tp-add、tp-remove、tp-clear、tp-get、tp-at、tp-member、match、regexp、search、navigation、interval 和 native query API 继续作为普通文本属性操作的直接 façade。
可复用声明配方
define-tp 和 define-tps 定义可以展开为直接 Emacs 属性的复用配方。它们只是 definition-time convenience,不是 live mounted layer,也不会把 identity 或 provenance 写入显示文本。
(define-tp link-style (foreground)
`(face (:foreground ,foreground :weight bold)
mouse-face highlight
help-echo "Open item"))
(tp-set "Project" '(link-style "#58a6ff"))
属性值需要求值时使用 tp-computed;任何普通函数对象仍然按 literal 保存。
(defvar my-height 1.2)
(define-tp sized-label ()
`(face ,(tp-computed (lambda () (list :height my-height)))))
computed function 在当前 binding/prepare context 中执行,因此其中的 tp-signal-read 和 tp-binding-read 会建立精确依赖。返回值只 normalize/project 一次,然后作为 literal data 使用。
响应式已有文本
tp-watch 装饰 marker-backed host range,但不拥有或替换其中的文字:
(let ((online (tp-signal-create nil)))
(with-current-buffer (get-buffer-create "*tp-status*")
(erase-buffer)
(insert "offline")
(tp-watch
(current-buffer) 1 8
(lambda ()
(list 'face
(list :foreground
(if (tp-signal-read online) "green" "red")))))))
signal 在被读取时记录真正依赖它的 binding。修改一个 signal 只会使它的订阅者失效;条件分支改变后,不再读取的 source 会自动取消订阅。相等写入是 no-op,tp-with-transaction 内的重复写入会让每个受影响 binding 最多重算一次。
tp-watch 返回底层 surface handle,可以交给 tp-surface-inspect、tp-surface-report 或 tp-surface-unmount。
Retained content
retained producer 接收 prepare context,在生成输出前取得稳定 object、安装 object-local binding,并返回纯 surface plan:
(let* ((status (tp-signal-create "ready"))
(producer
(lambda (context)
(let* ((object (tp-object-ensure context nil 'status 'text))
(binding
(tp-bind object '(demo . status)
(lambda () (tp-signal-read status)))))
(tp-surface-plan-create
:key 'status
:kind 'text
:text (tp-binding-read binding)
:props '(face bold)
:capability 'content))))
(buffer (get-buffer-create "*tp-retained*"))
(surface
(tp-surface-mount buffer producer '(:capability content))))
(tp-signal-set status "done")
(tp-surface-report surface))
plan 字段只有 key、kind、text、props、children、tags 和 capability,不包含 marker、buffer position、patch operation、producer closure 或 client continuation。普通 constructor 会防御性复制调用者持有的 string 和 property data;candidate-local producer 只有在转移所有嵌套值且不再暴露它们时,才能使用显式的 -owned plan/result constructor。Owned result 必须绑定当前 active prepare context,并且只消费一次。
稳定身份只在一个 surface 内有效。tp-object-ensure 按 parent、sibling key 和 kind reconcile;tp-object-resolve 按 key path 返回 live opaque handle。tp-surface-update-scoped 接收完整 candidate,但只授权一个或多个 retained object 当前 mount 范围内的输出变化;除非调用者显式选择 root fallback,否则越界变化会直接失败。
tp-surface-materialize-string 使用相同 producer/plan 语义生成一次性 string,但不建立 live surface。返回前会释放 ephemeral object、binding、subscription 和 anchor。
Host range 与属性所有权
properties capability 使用 opaque range anchor。producer 通过 tp-range-anchor-create 创建 anchor,再用 tp-object-attach-range 挂到 object 上。marker 跟随 host edit,而 plan 仍然不携带位置。
TP 为每个 property interval 保存 host baseline 和各个 TP contribution。重叠 contribution 通过已注册的 property policy 合成。如果外部代码在 TP publication 后修改同名属性,下一次更新会报告 tp-property-conflict,不会覆盖外部值。tp-range-rebase 显式接受当前 host 值作为新 baseline。unmount 只恢复仍由 TP 拥有的值,并保留冲突的 host edit。
事务与失败语义
tp-with-transaction 批量处理 signal writes 和所有实际受影响的 surfaces。TP 先 prepare 所有 candidate,再按稳定顺序 publish。compute、validation、buffer write、marker/index publication 或 transaction participant 任一步失败,signal values、binding values/dependencies、文本、属性、marker、index、plan、client state、revision 和 previous report 都会一起恢复。
observer 只在成功提交之后运行,observer failure 只记录,不回滚已完成的 transaction。publication 过程中被 kill 的 buffer 保持死亡,rollback 不会把它重新创建。
TP 默认由 structured route 从 transaction-scoped batch 的 exact entries、
同一个冻结 participant vector 和 candidate-bound final accept 驱动唯一 live
publication;journal、surface snapshot 与 change group 仍只保留一份。outer
transaction 开始前把 tp-transaction-execution-route 设为 v1,即可使用完整
legacy writer;route 会在该 transaction 内冻结,两条 writer 分支始终互斥。
shadow mode 只对已选择的单一路径生成只读兼容证据,不会建立第二个 change
group,也不会双写 Buffer。generic opaque authority marker 在 final accept 前
完成固定上界与 whitelist 校验;partial apply 或 accept failure 时先逆序恢复
marker,再执行普通 rollback。tp-with-transaction 仍返回 body result,内部
outcome 仅作为只读 side-channel evidence。
ETAF 集成复用同一事务边界,只注册一个 opaque transaction participant。ETAF 先准备 immutable generation 和 Ebox candidate,再由 TP 按固定顺序完成 participant publish、TP final accept 和 post-accept cleanup。participant failure 会恢复旧 generation 与 client state;accept 之后的 observer failure 只保留为 diagnostic。ETAF 的 Runtime fixed-point guard 会在一次 flush 中记录每个 effect 的 input/version tuple,重复或超过图规模上限时停止循环,不会静默自旋。
Property policy
tp-define-property-policy 为一个最终 Emacs text property 注册通用语义:
- normalization 与 validation;
- no-op 判断使用的 equality;
- contribution merge;
- 到最终 property value 的 projection;
- 包括 present
nil在内的显式 presence。
tp-register-text-property 为原生 property 建立默认 policy。face contribution 使用 merge 语义,其他属性使用各自注册的 policy。tp-merge-declarations、tp-define-style 和 tp-style-declarations 只处理 direct declarations,不实现 CSS cascade。
公共 runtime API 家族
| 家族 | 主要公共 API |
|---|---|
| Core inspection 与 debug | tp-debug-*、tp-intervals、tp-intervals-map、tp-plist、tp-text-snapshot、tp-empty-p |
| Property policy 与 declaration | tp-define-property-policy、tp-register-text-property、tp-text-declarations、tp-computed、tp-resolve-value、tp-merge-declarations、tp-define-style、tp-style-declarations |
| 静态 recipe | define-tp/tp-define-layer、define-tps/define-tp-group/tp-define-group、layer/group 查询、undefine/reset/describe |
| Signal 与 binding | signal create/read/peek/set/dispose、binding install/read/dispose、tp-variable-signal、tp-with-transaction、tp-transaction-participate、tp-transaction-participate-v2、只读 tp-transaction-active-p、tp-runtime-manifest、counter/reset |
| Object 与 plan | plan/result constructor、tp-object-ensure、retain/reuse、fragment/content-range attach、resolve、mounted/mounts |
| Host range | tp-range-anchor-create、tp-range-anchor-live-p、tp-object-attach-range、tp-range-rebase |
| Surface | mount/update/scoped update、materialize、live/revision/client-state、at-point、report/report-summary/inspect、unmount |
| Direct façade | tp-propertize、tp-apply、tp-watch、tp-set、tp-reset、tp-add、tp-remove、tp-clear、tp-get、tp-at、tp-member |
| Search 与 navigation | tp-match-*、tp-regexp-*、tp-search、tp-search-map、tp-forward*、tp-backward* |
| Native query 与 mutation | tp-lookup、tp-property-change、tp-property-any、tp-property-not-all、tp-with-mutation-policy |
| Palette 与 display helper | palette definition/lookup、built-in recipe、tp-palette-show、tp-pop-to-buffer、tp-switch-to-buffer、tp-display-buffer-mode |
完整签名、返回值、示例和模块索引见 API 参考; ownership、生命周期、错误和返回值合同见 API semantics, 模块边界和完整事务流程见 architecture。
TP 1.0 迁移
TP 1.0 直接删除 0.3 的 managed stack/renderer runtime,不使用隐藏兼容分支。删除的行为包括 tp-render.el、tp-stack.el、stack mutation APIs、tp-text、$variable declarations、layer-to-buffer registry、scan-driven refresh、managed attach/detach/diagnostics,以及以 tp-name/tp-layers/tp-meta 作为权威 runtime storage 的机制。
可复用静态声明使用 direct recipe;已有文本的响应式属性使用 tp-watch;响应式文字或结构化 UI 使用 retained content surface。TP 不会自动扫描历史 propertized text 来重建 runtime identity。
示例
这些示例只调用 TP public API,并在没有 Ebox 或 ECSS load-path 的环境中测试。
验证
make test
make test-shuffled SHUFFLE_SEED=20260806
make doctest
make compile-all WERROR=t
make checkdoc
make package-lint
make diff-check
结构测试还会证明:正常 signal/surface update 不扫描 buffer-list,不从显示文本搜索 identity;相等结果不会发布新 revision;unmount/kill cleanup 会释放 subscriptions 和 marker-backed runtime state。
许可证
GPL-3.0-or-later,详见 LICENSE。