tp/README_CN.md
Kinneyzhang 0d35358e05 refactor(tp)!: implement retained reactive runtime
Replace the legacy managed layer renderer with one independent retained/reactive text runtime. TP now owns exact dependencies, stable objects, marker-backed mounts, property contribution composition, atomic publication, rollback, and direct text-property facades without ECSS or Ebox dependencies.\n\nBREAKING CHANGE: remove tp-render, tp-stack, scan-driven managed layers, inline runtime metadata, TP-owned CSS cascade APIs, and dollar-variable declarations.\n\nVerified: 290/290 ERT, shuffled 290/290 (seed 20260806), 8/8 doctests, WERROR compile-all, checkdoc, package-lint, diff-check, and isolated TP-only load.
2026-08-07 00:39:50 +08:00

10 KiB
Raw Blame History

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

运行要求

  • 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-settp-resettp-addtp-removetp-cleartp-gettp-attp-member、match、regexp、search、navigation、interval 和 native query API 继续作为普通文本属性操作的直接 façade。

可复用声明配方

define-tpdefine-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-readtp-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-optp-with-transaction 内的重复写入会让每个受影响 binding 最多重算一次。

tp-watch 返回底层 surface handle可以交给 tp-surface-inspecttp-surface-reporttp-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 字段只有 keykindtextpropschildrentagscapability,不包含 marker、buffer position、patch operation、producer closure 或 client continuation。constructor 会防御性复制调用者持有的 string 和 property data。

稳定身份只在一个 surface 内有效。tp-object-ensure 按 parent、sibling key 和 kind reconciletp-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 不会把它重新创建。

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-declarationstp-define-styletp-style-declarations 只处理 direct declarations不实现 CSS cascade。

公共 runtime API 家族

家族 主要公共 API
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
Signal 与 binding tp-signal-create, tp-signal-read, tp-signal-peek, tp-signal-set, tp-bind, tp-binding-read, tp-with-transaction, tp-reactive-counters
Object 与 plan tp-object-ensure, tp-object-retain, tp-object-attach-fragment, tp-object-resolve, tp-object-mounts, tp-surface-plan-create, tp-surface-result-create
Host range tp-range-anchor-create, tp-object-attach-range, tp-range-rebase
Surface tp-surface-materialize-string, tp-surface-mount, tp-surface-update, tp-surface-update-scoped, tp-surface-unmount, tp-surface-at-point, tp-surface-report, tp-surface-inspect
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

ownership、生命周期、错误和返回值合同见 API semantics,模块边界和完整事务流程见 architecture

TP 1.0 迁移

TP 1.0 直接删除 0.3 的 managed stack/renderer runtime不使用隐藏兼容分支。删除的行为包括 tp-render.eltp-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相等结果不会发布新 revisionunmount/kill cleanup 会释放 subscriptions 和 marker-backed runtime state。

许可证

GPL-3.0-or-later详见 LICENSE