diff --git a/.gitignore b/.gitignore index ce14131..f46255a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Dependencies dash.el +postmortem # Emacs compiled files *.elc diff --git a/README.md b/README.md index dc612b9..df4785c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ TP does not depend on Ebox or ECSS. It does not implement CSS selectors, stylesh Chinese documentation: [README_CN.md](README_CN.md). +Complete public API reference: [API-REFERENCE.md](docs/API-REFERENCE.md) (中文). +It is the symbol-level usage index for the current TP 1.0 implementation; this +README remains the conceptual quick start. + ## Requirements - Emacs 28.1 or newer. @@ -124,7 +128,7 @@ A retained producer receives a prepare context, ensures stable objects before pr (tp-surface-report surface)) ``` -Plan fields are `key`, `kind`, `text`, `props`, `children`, `tags`, and `capability`. Plans contain no markers, buffer positions, patch operations, producer closures, or client continuations. Constructors defensively copy caller-owned strings and property data. +Plan fields are `key`, `kind`, `text`, `props`, `children`, `tags`, and `capability`. Plans contain no markers, buffer positions, patch operations, producer closures, or client continuations. Normal constructors defensively copy caller-owned strings and property data; candidate-local producers may use the explicit `-owned` plan/result constructors when they transfer every nested value and stop exposing it. An owned result must be created for the active prepare context and is consumed once. Stable identity is surface-local. `tp-object-ensure` reconciles by parent, sibling key, and kind; `tp-object-resolve` returns a live opaque handle by key path. `tp-surface-update-scoped` authorizes a full candidate update against one or more retained objects and rejects output changes outside their current mount ranges unless the caller explicitly selects root fallback. @@ -158,14 +162,22 @@ Observers run only after a successful commit. Their failures are recorded and do | Family | Main public APIs | | --- | --- | +| Core inspection and debug | `tp-debug-*`, `tp-intervals`, `tp-intervals-map`, `tp-plist`, `tp-text-snapshot`, `tp-empty-p` | | Property policy and declarations | `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` | -| Signals and bindings | `tp-signal-create`, `tp-signal-read`, `tp-signal-peek`, `tp-signal-set`, `tp-bind`, `tp-binding-read`, `tp-with-transaction`, `tp-reactive-counters` | -| Objects and plans | `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 ranges | `tp-range-anchor-create`, `tp-object-attach-range`, `tp-range-rebase` | -| Surfaces | `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` | +| Static recipes | `define-tp`/`tp-define-layer`, `define-tps`/`define-tp-group`/`tp-define-group`, layer/group queries, undefine/reset/describe | +| Signals and bindings | signal create/read/peek/set/dispose, binding install/read/dispose, `tp-variable-signal`, `tp-with-transaction`, `tp-transaction-participate`, counters/reset | +| Objects and plans | plan/result constructors, `tp-object-ensure`, retain/reuse, fragment/content-range attachment, resolve, mounted/mounts | +| Host ranges | `tp-range-anchor-create`, `tp-range-anchor-live-p`, `tp-object-attach-range`, `tp-range-rebase` | +| Surfaces | 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 and navigation | `tp-match-*`, `tp-regexp-*`, `tp-search`, `tp-search-map`, `tp-forward*`, `tp-backward*` | +| Native query and mutation | `tp-lookup`, `tp-property-change`, `tp-property-any`, `tp-property-not-all`, `tp-with-mutation-policy` | +| Palette and display helpers | palette definition/lookups, built-in recipes, `tp-palette-show`, `tp-pop-to-buffer`, `tp-switch-to-buffer`, `tp-display-buffer-mode` | -See [API semantics](docs/API-SEMANTICS.md) for ownership, lifecycle, error, and return contracts, and [architecture](docs/ARCHITECTURE.md) for module boundaries and transaction flow. +See the [complete API reference](docs/API-REFERENCE.md) for signatures, return +shapes, examples, and the full module index. See [API semantics](docs/API-SEMANTICS.md) +for ownership, lifecycle, error, and return contracts, and [architecture](docs/ARCHITECTURE.md) +for module boundaries and transaction flow. ## TP 1.0 migration diff --git a/README_CN.md b/README_CN.md index e63c0ed..2d7547c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -6,6 +6,9 @@ TP 不依赖 Ebox 或 ECSS,也不实现 CSS selector、stylesheet、specificit 英文文档:[README.md](README.md)。 +完整公共 API 参考:[API-REFERENCE.md](docs/API-REFERENCE.md)。本文负责概念 +和快速开始;API 参考按当前 TP 1.0 源码列出入口、参数语义、返回值和用法。 + ## 运行要求 - Emacs 28.1 或更高版本。 @@ -124,7 +127,7 @@ retained producer 接收 prepare context,在生成输出前取得稳定 object (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。 +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,否则越界变化会直接失败。 @@ -158,14 +161,21 @@ observer 只在成功提交之后运行,observer failure 只记录,不回滚 | 家族 | 主要公共 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` | +| 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`、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` | -ownership、生命周期、错误和返回值合同见 [API semantics](docs/API-SEMANTICS.md),模块边界和完整事务流程见 [architecture](docs/ARCHITECTURE.md)。 +完整签名、返回值、示例和模块索引见 [API 参考](docs/API-REFERENCE.md); +ownership、生命周期、错误和返回值合同见 [API semantics](docs/API-SEMANTICS.md), +模块边界和完整事务流程见 [architecture](docs/ARCHITECTURE.md)。 ## TP 1.0 迁移 diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md new file mode 100644 index 0000000..7cdce61 --- /dev/null +++ b/docs/API-REFERENCE.md @@ -0,0 +1,597 @@ +# TP 1.0 公共 API 与用法参考 + +本文是 TP 当前实现的完整公共入口索引。它以 tp.el 加载的模块为准;带 +tp-- 前缀的函数、变量和结构体是内部实现,不属于本文的稳定 API。 + +- 快速开始与能力边界:[README](../README.md)、[中文 README](../README_CN.md) +- 语义合同:[API-SEMANTICS.md](API-SEMANTICS.md) +- 当前架构:[ARCHITECTURE.md](ARCHITECTURE.md) +- 可运行示例:[examples/](../examples/) + +## 1. 运行边界 + +TP 只负责 Emacs text properties、响应式 signal/binding、retained object、 +marker-backed mount、diff、事务和 Buffer publication。它不实现 CSS selector、 +stylesheet、specificity、cascade、Box、Flex、Grid、测量或布局,也不依赖 +Ebox/ECSS。 + +所有坐标都遵循 Emacs 原生对象的坐标规则: + +- string 使用从 0 开始的 [start, end); +- buffer 使用从 1 开始的 [start, end); +- tp-surface-plan 中的 content leaf range 是相对于该 leaf 文本的 + 0-based offset,不是 Buffer position。 + +普通函数值、callback、keymap command 和 list 都是 literal data。只有显式的 +tp-computed source 会被执行。显式存在且值为 nil 与属性不存在始终不同。 + +## 2. 先选入口 + +| 目标 | 入口 | 是否建立 live state | +| --- | --- | --- | +| 创建带属性的 string | tp-propertize | 否 | +| 一次性改已有范围 | tp-apply、tp-set、tp-reset、tp-add、tp-remove、tp-clear | 否 | +| 查询属性和区间 | tp-get、tp-at、tp-member、tp-lookup | 否 | +| 按文字或属性搜索 | tp-match-*、tp-regexp-*、tp-search*、tp-forward/backward | 否 | +| 让已有 host text 响应变化 | tp-watch | 是,properties | +| 管理 TP 自己拥有的文字 | tp-surface-mount / tp-surface-update | 是,content | +| 检查或卸载 publication | tp-surface-inspect / tp-surface-report / tp-surface-unmount | 是 | +| 定义可复用声明 | define-tp、define-tps | 只建立 recipe,不建立 live state | +| 定义主题颜色 | define-tp-palette | 只更新 palette registry | + +安装只需要把仓库加入 load-path: + +~~~elisp +(add-to-list 'load-path "/path/to/tp") +(require 'tp) +~~~ + +tp.el 会加载所有运行时模块。单独加载模块只适合包开发或测试。 + +## 3. 一次性文本属性操作 + +### 3.1 创建和应用 + +~~~elisp +(tp-propertize + "Hello" + '(face bold help-echo "greeting" keymap nil)) + +(with-current-buffer (get-buffer-create "*tp-demo*") + (erase-buffer) + (insert "abcdef") + (tp-apply (current-buffer) 2 5 '(face italic))) +~~~ + +tp-propertize 返回原 string 的副本;tp-apply 保留范围内未出现在 +declarations 中的其他属性,并返回 (START . END)。 + +declarations 可以是原生 property plist、define-tp recipe、recipe call, +或包含 recipe key 的 plist。tp-apply 和下面的直接操作不会创建 object、 +binding、marker、subscription 或 surface。 + +### 3.2 set/reset/add/remove/clear + +对完整 string 的形式会返回新 string;对 Buffer 范围的形式会原地修改,并 +返回 (START . END)(tp-remove 和 tp-clear 的 Buffer 形式返回 nil)。 + +~~~elisp +;; string:property/value 形式,原 string 不变 +(tp-set "item" 'face 'bold) +(tp-reset "item" 'face 'italic) +(tp-add "item" 'face 'underline) +(tp-remove (tp-set "item" 'face 'bold) 'face) + +;; buffer 或 string 范围:一个 direct property plist,可选 OBJECT +(tp-set start end '(face bold help-echo "tip") buffer) +(tp-reset start end '(face italic) buffer) +(tp-add start end '(face (:weight bold)) buffer) +(tp-remove start end 'help-echo buffer) +(tp-clear start end buffer) +~~~ + +- tp-set 替换所指定的顶层属性; +- tp-reset 用 declarations 完全替换范围内所有属性; +- tp-add 合并属性;face、font-lock-face、mouse-face 支持 contribution + 合成和嵌套 plist 合并; +- tp-remove 支持移除顶层属性,也支持 (property sub-key) 或 + (property sub-key nested-key...); +- tp-clear 移除范围内全部 text properties。 + +## 4. 声明、property policy 和 recipe + +### 4.1 Property policy + +~~~elisp +(tp-define-property-policy + 'text/my-property + :normalizer #'identity + :validator (lambda (_value) t) + :equality #'equal + :merge (lambda (_old new) new) + :projector #'identity) + +(tp-property-policy 'text/my-property) +(tp-register-text-property 'help-echo) +(tp-text-property-id 'help-echo) +(tp-text-declarations '(face bold help-echo "tip")) +~~~ + +tp-define-property-policy 的 id 必须是包含 / 的 canonical symbol。可选策略 +为 :normalizer、:validator、:equality、:merge、:projector。注册是原子的; +无效策略不会替换旧定义。 + +- tp-register-text-property 为原生 Emacs property 安装默认 policy; +- tp-text-property-id 把原生 property 映射为 text/PROPERTY; +- tp-text-declarations 把原生 plist 转成 canonical declarations; +- tp-merge-declarations 按参数顺序合并 direct declaration groups,后者 + 覆盖前者,并保留显式 nil; +- tp-computed 把一个函数标记为唯一允许执行的 value source; +- tp-resolve-value 解析 literal 或显式 computed value; +- tp-define-style、tp-style-declarations、tp-undefine-style 管理已归一化的 + 命名 direct style。 + +### 4.2 静态 recipe + +~~~elisp +(define-tp link-style (color) + (list 'face (list :foreground color :weight 'bold) + 'mouse-face 'highlight)) + +(define-tps status-group () + link-style + '(help-echo "status")) + +(tp-set 1 5 '(link-style "#58a6ff") buffer) +(tp-layer-props-with-arg 'link-style "#58a6ff") +(tp-group-props 'status-group) +~~~ + +入口及别名: + +- define-tp(别名 tp-define-layer)定义一个返回原生 property plist 的 recipe; +- define-tps(别名 define-tp-group、tp-define-group)定义由多个 recipe、 + recipe call、原生 plist 或命名元素组成的 declaration group; +- tp-layer-parameterized-p、tp-layer-arglist、tp-layer-props、 + tp-layer-props-with-args、tp-layer-props-with-arg 查询或展开 layer; +- tp-group-parameterized-p、tp-group-props、tp-group-props-with-args、 + tp-group-props-with-arg 查询或展开 group; +- tp-undefine-layer、tp-undefine-group、tp-layer-reset 删除 recipe; +- tp-describe-layer 返回 recipe 的声明式诊断数据。 + +recipe 是 definition-time convenience,不是 mounted layer,也不会向文本 +写入 identity、provenance、tp-name 或 tp-layers。旧的 $variable reactive +syntax 会被拒绝。 + +tp-layer-alist 和 tp-layer-groups 是 recipe registry;通常通过上面的 query、 +undefine 和 reset API 访问,不要直接修改 registry。 + +## 5. Reactive signal 和 binding + +### 5.1 Signal + +~~~elisp +(let ((online (tp-signal-create nil))) + (tp-signal-read online) ; 在 binding/producer 中收集依赖 + (tp-signal-peek online) ; 读取但不收集依赖 + (tp-signal-set online t) + (tp-signal-subscriber-count online) + (tp-signal-live-p online) + (tp-signal-dispose online)) +~~~ + +tp-signal-create 的关键字选项: + +- :equality,默认 equal;相等写入是 no-op; +- :scope,默认 global,也可以是一个 live buffer。Buffer kill 会释放该范围 + 内 signal。 + +tp-signal-read 只在当前 computation 中登记 dependency;tp-signal-peek 不会 +登记。disposed signal 的读取会报错。 + +### 5.2 Binding + +~~~elisp +(let* ((source (tp-signal-create 0)) + (owner (list 'owner)) + (binding + (tp-bind owner 'value + (lambda () (tp-signal-read source)) + :lifecycle 'delete))) + (tp-binding-read binding) + (tp-binding-live-p binding) + (tp-binding-dependency-count binding) + (tp-binding-subscriber-count binding) + (tp-binding-owner-bindings owner) + (tp-binding-dispose binding) + (tp-signal-dispose source)) +~~~ + +tp-bind 按 OWNER + KEY 幂等安装 computation。KEY 应使用调用者自己的命名空间; +:equality 默认 equal;:lifecycle 为 delete 或 retain。binding 依赖集合按每次 +实际读取重建,因此条件分支会自动断开不再使用的 source。tp-binding-read +会在需要时计算并把 binding-to-binding 依赖登记到当前 computation。 + +tp-binding-dispose-owner 释放 owner 的全部 binding,返回释放数量; +tp-binding-dispose 释放单个 binding。 + +### 5.3 Transaction 和变量适配器 + +~~~elisp +(let ((left (tp-signal-create 0)) + (right (tp-signal-create 0))) + (tp-with-transaction + (tp-signal-set left 1) + (tp-signal-set right 2)) + (tp-signal-dispose left) + (tp-signal-dispose right)) + +(tp-transaction-participate + 'my-external-state + (lambda () (my-publish)) + (lambda () (my-rollback))) + +(tp-variable-signal 'my-variable) +(tp-variable-signal 'my-buffer-variable some-buffer) +(tp-reactive-counters) +(tp-reactive-reset-counters) +(tp-reactive-reset) +~~~ + +tp-with-transaction 将 signal、binding、surface 和注册的 transaction +participant 一起原子处理。participant 必须在 active transaction 内注册; +publish 在 surface publication 后、source commit 前运行,失败时按逆序 +rollback。tp-variable-signal 用 Emacs variable watcher 适配全局或指定 +Buffer 的变量,不是旧的 $variable API。 + +## 6. Retained surface + +### 6.1 Plan、result 和 object + +~~~elisp +(tp-surface-plan-create + :key 'label :kind 'text :text "Ready" + :props '(face bold) :tags '(:role status) + :capability 'content) +~~~ + +Plan 字段为 key、kind、text、props、children、tags、capability。leaf 使用 +text,容器使用 children;同一层的 key 必须唯一。capability 是 content 或 +properties。 + +- tp-surface-plan-create 防御性复制 caller-owned 数据; +- tp-surface-plan-create-owned 直接接收 candidate-local 数据;调用者必须 + 转移所有权,之后不得修改或向其他 owner 暴露; +- tp-surface-result-create 返回携带 plan 和 opaque client-state 的 producer + result; +- tp-surface-result-create-owned 只可对当前 active prepare context 使用, + 结果只消费一次。 + +producer 接收一个 prepare context,并应返回 plan 或 result: + +~~~elisp +(lambda (context) + (let ((object (tp-object-ensure context nil 'row 'text))) + (tp-surface-plan-create + :key 'row :kind 'text :text "row" + :props '(face italic) + :capability 'content))) +~~~ + +producer 不得在 prepare 阶段直接修改 live surface buffer;plan 不携带 +position、marker、patch operation、producer closure 或 continuation。 + +对象入口: + +- tp-object-ensure 按 parent、sibling key、kind 返回或创建当前 candidate 的 + 稳定 opaque object; +- tp-object-live-p、tp-object-mounted-p 检查 object 状态; +- tp-object-resolve 用 key path 从 surface side index 取得 object,不扫描文本; +- tp-object-mounts 返回 :start、:end、:tags 的数值快照; +- tp-object-retain 保留没有输出 fragment 的 object; +- tp-object-retain-subtree 保留已证明未删除的 object 子树; +- tp-object-reuse-subtree 复用已证明完全不变的 object 子树; +- tp-object-attach-fragment 把一个 object 绑定到 content fragment; +- tp-object-attach-content-range 把 object 绑定到 leaf 的相对字符范围; +- tp-object-attach-content-ranges 批量绑定 range,并复制 tags; +- tp-object-attach-content-ranges-owned 批量绑定 candidate-owned tags。 + +retain-subtree/reuse-subtree 不会替调用者证明 candidate 完整或输出未变化; +producer 仍须提交完整 plan,并承担“不变”证明。 + +### 6.2 Mount、update、scope 和 report + +~~~elisp +(let* ((surface + (tp-surface-mount + buffer producer + '(:capability content + :start 1 :end 1 + :client-state initial-state + :inhibit-read-only t + :observers (my-after-commit))))) + (tp-surface-update surface producer) + (tp-surface-update-scoped + surface + (list object) + producer + '(:on-mismatch error :return-report t)) + (tp-surface-report surface) + (tp-surface-report-summary surface) + (tp-surface-inspect surface) + (tp-surface-unmount surface)) +~~~ + +tp-surface-mount 的 plan-or-producer 可以是 plan、result 或 producer; +producer 的调用上下文由 TP 管理。properties surface 只能写属性,content +surface 才能拥有和更新文字。可用 mount options: +:capability、:start、:end、:inhibit-read-only、:coordinate-mounts、 +:client-state、:observers。 + +tp-surface-update 发布下一份完整 candidate。tp-surface-update-scoped 仍要求 +producer 生成完整 candidate,但只授权指定 object 当前 mount 范围; +:on-mismatch 默认报 tp-scope-mismatch,设为 root 才允许显式 root fallback; +:return-report 控制是否返回 report。 + +其他 surface API: + +- tp-surface-live-p、tp-surface-revision、tp-surface-client-state; +- tp-surface-materialize-string 使用相同 producer/plan 语义生成一次性 string, + 但不保留 surface、object、binding、subscription 或 anchor; +- tp-surface-at-point 通过 side index 返回当前位置的 live objects; +- tp-surface-report 返回完整 defensive report; +- tp-surface-report-summary 只返回 scalar commit metrics; +- tp-surface-inspect 返回 surface、capability、revision、object/mount 数量、 + client state 和 report; +- tp-surface-unmount 卸载并返回 report。Content 会删除自己的 span; + properties 只撤销仍由 TP 拥有的 contribution。 + +Report 的常用字段包括: +:transaction-id、:surface-id、:old-revision、:new-revision、 +:candidate-source-writes、:invalidated-bindings、:recomputed-bindings、 +:skipped-bindings、:reconciled-objects、:created-objects、:removed-objects、 +:moved-objects、:text-operations、:property-operations、 +:touched-characters、:full-root、:scope-count、:scope-range-count、 +:scope-fallback、:property-conflicts、:rolled-back、:failure、 +:observer-errors、:timing。 + +### 6.3 Host range 和 tp-watch + +~~~elisp +(let ((anchor + (tp-range-anchor-create + buffer start end + :start-insertion-type nil + :end-insertion-type t + :boundary-policy 'shorten))) + ;; 在 producer 的 context 中: + (tp-object-attach-range context object anchor) + (tp-range-anchor-live-p anchor) + (tp-range-rebase anchor)) +~~~ + +boundary-policy 可为 stale、shorten、remove。anchor 是 marker-backed opaque +range;host text 编辑由 marker 跟随,TP 不扫描附近文字猜测位置。 +tp-range-rebase 明确接受当前 host properties 作为新 baseline。 + +tp-watch 是 properties-only surface 的便捷入口: + +~~~elisp +(let ((surface + (tp-watch + buffer start end + (lambda () + (list 'face + (if enabled 'success 'shadow)))))) + (tp-surface-report surface)) +~~~ + +compute 是零参数函数,返回原生 declarations。文字由 host 所有;返回值是 +底层 properties surface,可交给 update/report/inspect/unmount。 + +## 7. 属性查询、区间和 mutation policy + +### 7.1 Direct query + +- tp-get 返回范围内的 (START END PROPS) property intervals;指定 property + 或 nested path 时只返回对应区间; +- tp-at 返回某一位置的完整 plist,或指定 property/nested path 的值; +- tp-member 只在 property 存在时返回 (PROPERTY VALUE),因此可区分 present + nil 和 absent; +- tp-intervals 返回对象范围的 property intervals,可用 absolute 请求 Buffer + 原生坐标; +- tp-intervals-map 对每个 interval 调用函数; +- tp-plist 返回位置或 string range 的合并 property plist; +- tp-empty-p 判断对象是否为空; +- tp-text-snapshot 返回 string 和 text properties 的防御性快照。 + +### 7.2 Native query + +~~~elisp +(tp-lookup position 'face + :object buffer + :mode :text-effective) +(tp-property-change position :property 'face :direction :next) +(tp-property-any start end 'face 'bold buffer) +(tp-property-not-all start end 'face 'bold buffer) + +(tp-with-mutation-policy '(:modified :silent :read-only :inhibit) + (tp-apply buffer start end '(face bold))) +~~~ + +tp-lookup 返回 tp-lookup-result,mode 可为: + +- :text-direct:直接 text property; +- :text-effective:考虑 category/default 等 text-property 继承; +- :text-source:返回实际 text source; +- :char:使用 get-char-property-and-overlay 的字符属性; +- :char-source:字符属性的实际来源,overlay 会记录为 :overlay。 + +result 的 present-p 区分显式 nil;tp-property-change 的 direction 为 :next +或 :previous,property 为 nil 时观察任意属性变化。 + +tp-with-mutation-policy 的 :modified 可为 :ordinary 或 :silent, +:read-only 可为 :respect 或 :inhibit。支持 ordinary/respect、 +ordinary/inhibit、silent/inhibit;silent/respect 会报错。 + +## 8. Match、regexp、search 和 navigation + +所有 search 都是 presence-aware:省略 VALUE 或显式传入 tp-any-value 匹配 +所有存在的值;显式 nil 只匹配“存在且值为 nil”。 + +### 8.1 按文字匹配并修改 + +- tp-match-set:literal pattern 的所有匹配上设置属性; +- tp-match-reset:literal pattern 的所有匹配上重置属性; +- tp-match-add:literal pattern 的所有匹配上合并属性; +- tp-regexp-set、tp-regexp-reset、tp-regexp-add:regexp 匹配,可用 subexp + 只操作 capture group。 + +Pattern 可以是一个 string 或 string 列表。String object 返回新 string; +Buffer object 原地修改并返回匹配范围列表。start > end 时边界会交换, +匹配不会跨出指定范围。 + +### 8.2 查找和批量处理 + +~~~elisp +(tp-search string 'status) +(tp-search start end 'status 'ready buffer) +(tp-forward 'status tp-any-value buffer 2) +(tp-backward 'status nil buffer) +(tp-search-map + (lambda (text _start _end _index) (upcase text)) + 'status tp-any-value buffer) +~~~ + +- tp-search 返回所有匹配的 (START END VALUE); +- tp-forward / tp-backward 在 Buffer 中从 point 搜索并返回 prop-match, + 在 string 中返回 0-based 区间列表; +- tp-forward-do / tp-backward-do 找到第 N 个匹配后只对最后一个执行 + function,不是 for-each; +- tp-search-map 对每个匹配执行 function。function 可返回替换 string、 + 修改 text 后的 string,或 nil 跳过。String 替换必须保持长度不变; + Buffer 替换可以改变长度; +- tp-search-forward / tp-search-backward 只是 Emacs + text-property-search-* 的 obsolete raw wrapper,新增代码使用 + tp-forward / tp-backward 或直接使用 Emacs primitive。 + +## 9. Palette、内置 recipe 和显示辅助 + +~~~elisp +(define-tp-palette status + :fg ("#22863a" . "#3fb950") + :bg ("#f0fff4" . "#1a2e1f")) + +(tp-palette-color 'status :fg) +(tp-palette-fg-color 'status) +(tp-palette-bg-color 'status) +(tp-palette-border-color 'status) +(tp-palette-has-p 'status :fg) +(tp-palette-pure 'status-fg) +(tp-set "Ready" 'tp-palette 'status-fg) +~~~ + +define-tp-palette(别名 tp-define-palette)注册 :fg、:bg、:border。颜色 +可以是 string、(LIGHT . DARK) 或 (:light LIGHT :dark DARK)。查询和主题工具: + +tp-palette-alist 是 palette lookup 的单一来源;使用 +define-tp-palette/tp-define-palette 更新它。 + +- tp-theme-dark-p、tp-theme-light-p、tp-parse-color; +- tp-palette-color、tp-palette-fg-color、tp-palette-bg-color、 + tp-palette-border-color; +- tp-palette-has-p 判断 palette 或 key 是否存在; +- tp-palette-p 判断 palette 名; +- tp-palette-fg-p、tp-palette-bg-p、tp-palette-fbg-p、tp-palette-border-p + 判断 NAME-* variant; +- tp-palette-pure 去掉 variant suffix; +- tp-palette-show 打开 *tp-palette-gallery*,按 q 退出。 + +TP 自带的 recipe 包括: + +tp-palette、tp-fg、tp-bg、tp-button、tp-underline、tp-delete、tp-link、 +tp-space、tp-headline、tp-action。 + +显示辅助: + +- tp-pop-to-buffer:清空并填充 buffer 后 pop-to-buffer,最后设为只读; +- tp-switch-to-buffer:同样的填充和只读行为,但使用 switch-to-buffer; +- tp-display-buffer-mode:为 TP 显示 buffer 提供 buffer-local 的 q 退出键, + 不修改共享 major-mode map。 + +tp-display-buffer-mode-map 是该 minor mode 的 keymap;默认只绑定 q 到 +quit-window。 + +## 10. Debug 和公共 record + +Debug 入口: + +- tp-debug-mode、tp-debug-echo 控制日志行为; +- tp-debug-log 记录格式化消息; +- tp-debug-clear 清空日志; +- tp-debug-show 显示日志; +- tp-with-current-buffer 在指定 Buffer 中执行 body; +- tp-face-properties 列出 face family properties。 + +以下公共结果类型由 cl-defstruct 提供 predicate 和字段 accessor。调用者不应 +直接调用内部 constructor;优先使用前文的 create/query API: + +- tp-property-policy:id、normalizer、validator、equality、merge、projector; +- tp-signal:id、committed-value、equality、scope、revision 等运行时字段; +- tp-binding:id、owner、key、compute、last-value、dependencies、lifecycle + 等运行时字段; +- tp-surface-plan:key、kind、text、props、children、tags、capability; +- tp-surface-result:plan、client-state 及 owned/consumed 状态; +- tp-lookup-result:property、value、present-p、source、mode、object、 + position、overlay。 + +## 11. 错误、生命周期和已删除 API + +重要错误类型: + +- core:tp-unsupported-buffer; +- property:tp-property-error、tp-invalid-property-policy、 + tp-invalid-declaration; +- recipe:tp-invalid-layer-definition、tp-unresolved-layer; +- reactive:tp-reactive-error、tp-invalid-signal-scope、tp-disposed-signal、 + tp-disposed-binding、tp-binding-cycle; +- surface:tp-surface-error、tp-invalid-surface-plan、tp-duplicate-object-key、 + tp-invalid-prepare-context、tp-stale-object、tp-cross-surface-object、 + tp-orphan-object、tp-capability-error、tp-stale-mount、 + tp-property-conflict、tp-dead-surface、tp-invalid-range-anchor、 + tp-invalid-content-range、tp-producer-buffer-mutation、 + tp-publication-mismatch、tp-owned-result-error、tp-scope-mismatch。 + +Live object、binding、surface、anchor 或 signal 被 dispose、unmount 或 +Buffer kill 后,继续使用会报相应 stale/disposed 错误。Observer 只在成功 +commit 后运行;observer error 会记录在 report,不会回滚已提交 publication。 + +TP 1.0 已删除并且不应在新代码中使用: + +- tp-render.el、tp-stack.el、managed stack mutation; +- tp-text、$variable、inline tp-name/tp-layers/tp-meta runtime database; +- layer-to-buffer registry、scan-driven refresh、managed attach/detach; +- tp-with-batch-updates 和旧 CSS/ECSS runtime integration。 + +因此,当前正确的组合是:静态声明用 recipe/direct API,已有文字的响应式 +属性用 tp-watch,TP 自己拥有的响应式文字用 content surface。 + +## 12. 完整模块索引 + +| 模块 | 当前公共入口 | +| --- | --- | +| tp-core.el | tp-debug-*、tp-with-current-buffer、tp-intervals、tp-intervals-map、tp-empty-p、tp-plist、tp-text-snapshot | +| tp-style.el | tp-define-property-policy、tp-property-policy、tp-text-property-id、tp-register-text-property、tp-text-declarations、tp-computed、tp-resolve-value、tp-merge-declarations、tp-define-style、tp-style-declarations、tp-undefine-style | +| tp-layer.el | define-tp/tp-define-layer、define-tps/define-tp-group/tp-define-group、layer/group query、tp-layer-reset、tp-undefine-*、tp-describe-layer | +| tp-reactive.el | signal、binding、transaction、variable adapter、counter 和 reset API | +| tp-surface.el | plan/result、object、range anchor、surface lifecycle、scoped update、report、tp-watch | +| tp-ops.el | tp-propertize、tp-apply、tp-set、tp-reset、tp-add、tp-remove、tp-clear、tp-get、tp-at、tp-member | +| tp-search.el | tp-match-*、tp-regexp-*、tp-search、tp-search-map、tp-forward*、tp-backward*、tp-any-value | +| tp-query.el | tp-lookup、tp-property-change、tp-property-any、tp-property-not-all、tp-with-mutation-policy | +| tp-palette.el | palette definition、theme/color lookup、palette predicates | +| tp-builtins.el | built-in recipes、tp-palette-show、display-buffer macros/mode | + +Makefile 提供 make test、make test-shuffled、make doctest、make compile-all WERROR=t、 +make checkdoc、make package-lint、make diff-check 和 make benchmark。实现细节 +和测试合同以 source、tests/、[API semantics](API-SEMANTICS.md) 和 +[architecture](ARCHITECTURE.md) 为准。 diff --git a/docs/API-SEMANTICS.md b/docs/API-SEMANTICS.md index 4992ba8..11f689a 100644 --- a/docs/API-SEMANTICS.md +++ b/docs/API-SEMANTICS.md @@ -2,6 +2,8 @@ 本文记录 TP 1.0 当前公共 API 的 ownership、presence、响应式、retained surface、事务与失败合同。它描述已经实现的行为;目标背景与设计理由见 [retained runtime architecture](retained-runtime-target-architecture.md)。 +完整的公共符号、参数形状、返回值和示例见 [API reference](API-REFERENCE.md)。 + ## 1. 产品边界 TP 负责最终 Emacs text property policy/contribution、signals/bindings、stable objects、marker-backed mounts、surface diff、transaction、rollback 和 Buffer publication。 @@ -111,7 +113,7 @@ Search/match/regexp/navigation/query API 继续委托 Emacs 原生 text-property | `tags` | opaque indexed metadata,TP 不解释其业务含义 | | `capability` | `content` 或 `properties` | -Plan 不允许 marker、buffer position、patch op、producer closure 或 binding closure。Constructor 防御性复制 string、props、children 与 tags,使 caller 后续 mutation 不改变 committed plan。 +Plan 不允许 marker、buffer position、patch op、producer closure 或 binding closure。Constructor 防御性复制 caller 提供的 key、kind、text、props、children list 与 tags,使 caller 后续 mutation 不改变 committed plan。`tp-surface-result-create` 只携带 plan 与 opaque client state;TP 在 prepare transaction 内从该 plan 生成唯一的 authoritative rendered output,不接受第二份 caller-provided text 或 traversal records。Result 与 producer normalization 会在各自边界建立防御性 plan snapshot。Producer 如果为当前 candidate freshly allocate 了完整 plan tree,并且之后不再 mutation 或 expose,可使用 `tp-surface-plan-create-owned` 与 `tp-surface-result-create-owned` 将 plan 所有权转给 TP,跳过重复 snapshot;owned result 必须绑定当前 active prepare context,并且只允许被 prepare normalization 消费一次;普通 caller-owned plan 必须使用复制入口。 producer 在 prepare 阶段接收 context,并在产生 plan 前调用: @@ -125,6 +127,12 @@ Candidate object 只有成功 publication 后才变为 live。失败 candidate h 无可见字符但需要保留的 logical object 使用 `tp-object-retain`。一个 logical object 可以通过 `tp-object-attach-fragment` 关联多个离散 plan fragments;attachment 保存在 prepare side state,不进入 plan。 +当 producer 已经证明一个现有 logical subtree 在本次 candidate 中保持不变,可以使用 `tp-object-retain-subtree` 复用当前 live root 下已知的 object path。该 API 会把 subtree objects 标记为 touched/retained,并保留其 live bindings;它不创建缺失对象,也不替 producer 验证 candidate plan、输出范围或布局不变性。调用方仍必须提交完整 candidate plan,并在不满足证明时回退到普通 `tp-object-ensure` 路径。若完整 candidate 仍会重新提交这些对象的 output attachment,但已证明它们的 identity、lifecycle 和 output 不变,可使用 `tp-object-reuse-subtree`:对象保留在 candidate 中但不进入 touched/reconcile 集合。 + +Content range ownership 有两个入口:`tp-object-attach-content-ranges` 对每个 `:tags` 建立防御性快照;只有当 producer 为当前 candidate 新建了 tags、之后不会再修改或暴露它们时,才可使用 `tp-object-attach-content-ranges-owned` 转移这些值的所有权并跳过复制。转移入口不改变失败 candidate 的 rollback 语义;未成功 publication 的值会随 candidate 一起丢弃。普通 caller-owned tags 必须使用复制入口。 + +`tp-surface-report` 始终返回完整的防御性 report snapshot。只需要 transaction、operation、scope 或 object reconciliation 计数等提交标量的 integration caller 应使用 `tp-surface-report-summary`,它不暴露 nested report payload,也不为读取这些标量复制整份 report。 + ## 8. Surface lifecycle ### 8.1 Materialize @@ -147,7 +155,7 @@ Mount options 支持 `:capability`、`:start`、`:end`、`:inhibit-read-only`、 `content` surface 拥有其 span 的 text 和 properties,可以插入、删除、替换或移动输出。`properties` surface 只能贡献声明的 properties,不能替换 host text。 -`tp-surface-update-scoped` 仍接收完整 candidate。TP 从 object-to-mount index 得到授权范围,验证 candidate 没有改变范围外输出,再在同一 transaction 发布。默认 mismatch 报 `tp-scope-mismatch`;`(:on-mismatch root)` 显式允许 full-root fallback。 +`tp-surface-update-scoped` 仍接收完整 candidate。TP 从 object-to-mount index 得到授权范围,验证 candidate 没有改变范围外输出,再在同一 transaction 发布。默认 mismatch 报 `tp-scope-mismatch`;`(:on-mismatch root)` 显式允许 full-root fallback。默认返回 defensive report snapshot;调用方如果只通过 `tp-surface-report-summary` 读取标量提交指标,可传 `(:return-report nil)` 避免构造完整 report snapshot。 相等 candidate 不产生 publication,surface revision 和 buffer modified state 保持不变。 @@ -204,11 +212,16 @@ TP 为每个 interval 保存: ## 13. Error ownership +Surface publication 还会使用 `tp-invalid-content-range`、 +`tp-producer-buffer-mutation`、`tp-publication-mismatch` 和 +`tp-owned-result-error`;它们分别归属于 content range 校验、prepare +buffer guard、publication journal 和 owned producer result 合同。 + 主要错误类型: - property/declaration:`tp-property-error`、`tp-invalid-property-policy`、`tp-invalid-declaration`、`tp-invalid-layer-definition`、`tp-unresolved-layer`; - reactive:`tp-reactive-error`、`tp-invalid-signal-scope`、`tp-disposed-signal`、`tp-disposed-binding`、`tp-binding-cycle`; -- retained surface:`tp-invalid-surface-plan`、`tp-duplicate-object-key`、`tp-invalid-prepare-context`、`tp-stale-object`、`tp-cross-surface-object`、`tp-orphan-object`、`tp-capability-error`、`tp-stale-mount`、`tp-property-conflict`、`tp-dead-surface`、`tp-invalid-range-anchor`、`tp-scope-mismatch`。 +- retained surface:`tp-surface-error`、`tp-invalid-surface-plan`、`tp-duplicate-object-key`、`tp-invalid-prepare-context`、`tp-stale-object`、`tp-cross-surface-object`、`tp-orphan-object`、`tp-capability-error`、`tp-stale-mount`、`tp-property-conflict`、`tp-dead-surface`、`tp-invalid-range-anchor`、`tp-invalid-content-range`、`tp-producer-buffer-mutation`、`tp-publication-mismatch`、`tp-owned-result-error`、`tp-scope-mismatch`。 内部 computation 不吞错或返回貌似合理的 fallback。只有用户入口和 batch test runner等外层边界负责把错误转换为展示信息。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 45f52b4..158608c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,6 +2,8 @@ 本文描述 TP 1.0 当前实现的模块边界、权威状态、数据流和事务模型。公共行为合同见 [API semantics](API-SEMANTICS.md),设计背景见 [retained runtime architecture](retained-runtime-target-architecture.md)。 +按功能查找公共入口和用法时,使用 [API reference](API-REFERENCE.md)。 + ## 1. 定位 TP 是通用 retained/reactive text runtime: @@ -110,7 +112,7 @@ Object identity is scoped to one surface and derived from: Candidate objects exist only inside the context. Successful publication promotes them to live objects; failed contexts dispose them and their bindings/anchors. `tp-object-resolve` queries live identity by key path without creating state. -The context records touched objects/bindings. Omitted objects are removed. Omitted bindings default to deletion unless an explicit lifecycle says retain. A logical object with no direct plan node must call `tp-object-retain`; disjoint physical output is attached through `tp-object-attach-fragment`. +The context records touched objects/bindings. Omitted objects are removed. Omitted bindings default to deletion unless an explicit lifecycle says retain. A logical object with no direct plan node must call `tp-object-retain`; disjoint physical output is attached through `tp-object-attach-fragment`. Content ranges normally use `tp-object-attach-content-ranges`, which defensively snapshots opaque tags. A producer that freshly allocates candidate-local tags and never mutates or exposes them may use `tp-object-attach-content-ranges-owned` to transfer those snapshots without another copy. The same candidate-local ownership rule applies to `tp-surface-plan-create-owned` and `tp-surface-result-create-owned`; an owned result is bound to the active prepare context and consumed once, while normal plan/result constructors remain defensive boundaries. ## 7. Pure surface plans @@ -170,7 +172,7 @@ TP reconciles object identity by the prepare tree and compares old/new plans for - mount/index changes; - scoped output authorization. -`tp-surface-update-scoped` maps requested objects directly through the object-to-mount index. For content surfaces it proves old/new changes stay within those mounted ranges; properties surfaces perform the equivalent contribution-range proof. A mismatch is an error unless root fallback is explicitly selected. +`tp-surface-update-scoped` maps requested objects directly through the object-to-mount index. For content surfaces it proves old/new changes stay within those mounted ranges; properties surfaces perform the equivalent contribution-range proof. A mismatch is an error unless root fallback is explicitly selected. Callers that only need scalar commit metrics may request no report snapshot and read `tp-surface-report-summary` after publication. An equal candidate produces no prepared publication. It preserves revision, report, buffer modified state, markers, and client state. diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 0998bf7..1338923 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -1,44 +1,69 @@ -# Reproducible benchmark evidence +# Reproducible benchmark guide -> **Historical TP 0.3 benchmark snapshot; obsolete for TP 1.0.** These results measure the removed managed stack, `tp-text`, layer registry, scan-driven renderer, and 0.3 benchmark runner. They are preserved as historical evidence only and are neither TP 1.0 performance baselines nor current release gates. Current runtime structure and verification expectations are documented in the [README](../README.md), [current architecture](ARCHITECTURE.md), and [API contract](API-SEMANTICS.md). +This document describes the benchmark runner shipped with TP 1.0. It measures +the current retained/reactive runtime; it is not a historical TP 0.3 stack +benchmark and it does not impose a release threshold. -## Command and environment +## Run -```sh -make benchmark \ - EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs \ - LOAD_EXTRA='-L /tmp/tp-elpa.Ur243x/dash-2.20.0' -``` +Use the Makefile entry point: -- Emacs: 30.2 -- Fixed seeds: `1`, `7`, `42`, `747555` -- Reproducible generated seed: `8675309` -- Every scenario performs a correctness assertion before accepting timing. -- The full command completed successfully on 2026-07-28. +~~~sh +make benchmark +~~~ -The runner is `tp-benchmark.el`; `make benchmark` is the stable entry point. Output rows include scenario, status, fixture, seed, requested/actual size, operations, scanned/changed/refreshed counts, elapsed wall time, and GC count. +The equivalent batch command is: -## Representative result: seed 42 +~~~sh +emacs -Q --batch -L . -l tp-benchmark.el -f tp-benchmark-run +~~~ -| Scenario | Requested / actual | Operations | Scanned | Changed | Refreshed | Elapsed (s) | GC | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | -| large text | 100,000 | 2 | 100,000 | 100,000 | 0 | 0.000304 | 0 | -| large text | 1,000,000 | 2 | 1,000,000 | 1,000,000 | 0 | 0.000323 | 0 | -| fragmented intervals | 1,000 | 1 | 1,000 | 500 | 0 | 0.044254 | 0 | -| fragmented intervals | 10,000 | 1 | 10,000 | 5,000 | 0 | 0.404836 | 1 | -| fragmented intervals | 50,000 | 1 | 50,000 | 25,000 | 0 | 1.965066 | 3 | -| stack depth | 1 | 2 | 2,000 | 2,000 | 0 | 0.000909 | 0 | -| stack depth | 5 | 6 | 2,000 | 2,000 | 0 | 0.003204 | 0 | -| stack depth | 20 | 21 | 2,000 | 2,000 | 0 | 0.012905 | 0 | -| stack depth | 50 | 51 | 2,000 | 2,000 | 0 | 0.037830 | 0 | -| reactive fan-out | 1 / 1 | 1 | 1 | 1 | 1 | 0.000763 | 0 | -| reactive fan-out | 10 / 10 | 1 | 10 | 10 | 10 | 0.005469 | 0 | -| reactive fan-out | 100 / 100 | 1 | 100 | 100 | 100 | 0.051376 | 0 | -| reactive fan-out | 500 / 200 | 1 | 200 | 200 | 200 | 0.102989 | 0 | -| theme refresh | 1 / 1 | 1 | 1,000 | 0 | 1 | 0.001317 | 0 | +The runner uses fixed seeds 1, 7, 42, 747555 and generated seed 8675309. Each +scenario performs a correctness assertion before timing the operation. Record +the Emacs version, machine, seed, and full output when comparing runs. -The 500-buffer fan-out fixture is intentionally capped at 200 live buffers on local hardware; output reports both requested and actual counts. Theme refresh uses the v1 conservative managed-range strategy. +## Scenarios + +For every seed, the runner executes: + +| Scenario | Fixtures | +| --- | --- | +| large-text | strings of 100,000 and 1,000,000 characters; set and presence-aware search | +| fragmented | 1,000, 10,000, and 50,000 alternating property intervals | +| retained-keyed-reconcile | retained content with 10, 100, and 1,000 stable keyed entries | +| signal-sparse-update | one target binding beside 1, 100, and 10,000 unrelated bindings | +| transaction-batch | 1, 100, and 10,000 writes to one retained surface | +| equal-write-noop | the same write counts, all equal to the committed value | + +The retained scenarios verify stable object reconciliation, correct published +text, dependency-local recomputation, one publication for a batch, and no +revision change for equal writes. A failed assertion aborts the run instead of +producing misleading timing evidence. + +## Output + +Each row is a whitespace-separated key/value record. The stable fields are: + +scenario, status, fixture, seed, requested, actual, operations, objects, +subscribers, invalidated, recomputed, skipped, text-operations, +property-operations, touched, revision, published, elapsed, gcs, and note. + +The output is deliberately machine-readable enough for local comparison, but +it is not a compatibility format. Interpret it together with the scenario +source in tp-benchmark.el. ## Interpretation -These figures are advisory, not release thresholds. Fragmented interval count is the clearest scaling risk and should remain the first optimization target. Large-text lookup over one property run and deep-stack top lookup are cheap in this fixture, but those results must not be generalized to fragmented text or repeated stack mutation. +These measurements are advisory. Runtime, garbage collection, Emacs build, +machine load, and buffer implementation details affect absolute timings. +Compare like-for-like runs, inspect correctness failures first, and use the +reports/counters to explain a regression: + +- fragmented measures expose interval-run scaling; +- retained-keyed-reconcile measures keyed object reuse and publication work; +- signal-sparse-update checks that unrelated bindings are not recomputed; +- transaction-batch measures deduplicated recomputation and one surface commit; +- equal-write-noop checks that equal values do not publish a new revision. + +For the contracts behind these scenarios, read [API semantics](API-SEMANTICS.md), +[architecture](ARCHITECTURE.md), and the [public API reference](API-REFERENCE.md). diff --git a/docs/CODE-ANALYSIS.md b/docs/CODE-ANALYSIS.md index 2694236..473277e 100644 --- a/docs/CODE-ANALYSIS.md +++ b/docs/CODE-ANALYSIS.md @@ -1,801 +1,12 @@ -# tp.el 代码分析报告 +# Code analysis(历史归档) -> **历史 TP 0.1/0.3 分析,TP 1.0 已废弃。** 本报告混合记录拆分前的单文件 TP 0.1 与后续 TP 0.3 模块状态,其中的 `tp-render.el`、`tp-stack.el`、`tp-text`、inline `tp-name`/`tp-layers` 和扫描式响应更新均已从 TP 1.0 删除。下文的文件结构、调用堆栈、代码位置、API 与“当前”状态只属于历史快照,不适用于现行实现;正文保持原样作为设计证据。当前事实见 [README](../README_CN.md)、[当前架构](ARCHITECTURE.md)、[API 合同](API-SEMANTICS.md) 与 [1.0 变更记录](../CHANGELOG.md)。 +这是早期 TP 0.1/0.3 的代码分析快照。它描述的 tp-render、tp-stack、tp-text、 +inline metadata 和 managed registry 已被 TP 1.0 删除,因此本文不再是当前 +实现的说明,也不应被用来推断可用 API。 -本报告旨在帮助想要参与 tp.el 开发的开发者快速了解项目结构、核心功能实现、以及潜在的优化方向。 +当前代码的功能、用法和模块边界请阅读: -## 目录 - -- [项目概述](#项目概述) -- [文件结构](#文件结构) -- [核心功能函数调用堆栈](#核心功能函数调用堆栈) - - [1. 文本属性设置 tp-set](#1-文本属性设置-tp-set) - - [2. 深度合并属性 tp-add](#2-深度合并属性-tp-add) - - [3. 属性层推送 tp-push-layer](#3-属性层推送-tp-push-layer) - - [4. 响应式层定义 define-tp](#4-响应式层定义-define-tp) - - [5. 响应式更新触发](#5-响应式更新触发) - - [6. 模式匹配 tp-match-set](#6-模式匹配-tp-match-set) - - [7. 搜索与遍历 tp-search-map](#7-搜索与遍历-tp-search-map) -- [关键数据结构](#关键数据结构) -- [潜在问题分析](#潜在问题分析) -- [架构优化建议](#架构优化建议) -- [开发入门指南](#开发入门指南) - ---- - -## 项目概述 - -tp.el 是一个 Emacs Lisp 文本属性操作库,拆分前的单文件版本采用概念上的 **五层架构设计**: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 第五层:高级 API │ -│ tp-match-set, tp-regexp-set, tp-forward-do, tp-search-map │ -├─────────────────────────────────────────────────────────────────┤ -│ 第四层:响应式系统 │ -│ define-tp, define-tps, tp--reactive-variable-watcher │ -├─────────────────────────────────────────────────────────────────┤ -│ 第三层:属性层系统 │ -│ tp-push-layer, tp-pop-layer, tp-rotate-layer │ -├─────────────────────────────────────────────────────────────────┤ -│ 第二层:核心属性操作 │ -│ tp-set, tp-reset, tp-add, tp-get, tp-at, tp-remove │ -├─────────────────────────────────────────────────────────────────┤ -│ 第一层:基础工具函数 │ -│ tp--parse-args, tp--deep-merge-plist, tp-intervals │ -└─────────────────────────────────────────────────────────────────┘ -``` - -0.2.0 的模块拆分大体沿用了这一分层思路,并把"响应式系统向上调用高级 API"的 -矛盾收拢为 tp-render.el 安装的钩子变量,详见 [ARCHITECTURE.md](ARCHITECTURE.md)。 - ---- - -## 文件结构 - -当前(0.3.0)文件结构: - -``` -tp/ -├── tp.el # 总入口,按序加载全部模块(62 行) -├── tp-core.el # 区间遍历、plist/face 合并引擎、调试、$var 工具(781 行) -├── tp-reactive.el # 响应式依赖注册表、变量监听、批量队列(370 行) -├── tp-layer.el # define-tp / define-tps、层注册表与解析(1295 行) -├── tp-ops.el # 核心属性操作 tp-set/get/at/remove/...(916 行) -├── tp-search.el # 模式匹配、搜索与导航(810 行) -├── tp-render.el # 响应式重渲染引擎(501 行) -├── tp-stack.el # 属性层栈操作(709 行) -├── tp-palette.el # 明/暗主题调色板数据(351 行) -├── tp-builtins.el # 内置层与辅助工具(193 行) -├── tests/ -│ ├── tp-tests.el # 综合 ERT 测试套件 -│ ├── tp-*-tests.el # 各模块回归测试套件 -│ ├── tp-doctest.el # README 可执行示例 -│ └── tp-run-shuffled.el # 乱序 ERT 运行器 -├── Makefile # test / compile / clean -├── docs/ -│ ├── ARCHITECTURE.md # 架构文档 -│ ├── CODE-ANALYSIS.md # 代码分析报告(本文档) -│ ├── reactive-text-properties.md # 响应式功能文档(中文) -│ ├── reactive-text-properties-en.md # 响应式功能文档(英文) -│ ├── reactive-optimization.md # 优化文档(中文) -│ └── reactive-optimization-en.md # 优化文档(英文) -├── README.md # 英文说明文档 -└── README_CN.md # 中文说明文档 -``` - ---- - -## 核心功能函数调用堆栈 - -### 1. 文本属性设置 tp-set - -`tp-set` 是最核心的属性设置函数,支持三种调用方式。 - -#### 调用堆栈 - -``` -tp-set (用户调用入口) - │ - ├─→ tp--parse-args (解析参数格式) - │ │ - │ └─→ tp--resolve-props (解析属性,包括层名称) - │ │ - │ ├─→ tp-layer-props (获取层定义的属性) - │ ├─→ tp--collect-reactive-symbols (收集 $var 符号) - │ ├─→ tp--resolve-reactive-symbols (解析为实际值) - │ └─→ tp--register-reactive-deps (注册响应式依赖) - │ - ├─→ tp--handle-tp-text-property (处理 tp-text 特殊属性) - │ │ - │ └─→ (替换文本内容,如果 tp-text 存在) - │ - └─→ put-text-property / propertize (Emacs 原生 API) -``` - -#### 关键代码位置 - -(0.2.0 模块化后按"函数 → 模块文件"定位;文件内具体行号请用 `M-x xref-find-definitions` 查找。) - -| 函数 | 模块文件 | 作用 | -|------|----------|------| -| `tp-set` | tp-ops.el | 主入口函数 | -| `tp--parse-args` | tp-ops.el | 解析多种调用格式 | -| `tp--resolve-props` | tp-layer.el | 展开层名称和响应式变量 | -| `tp--handle-tp-text-property` | tp-render.el | 处理 tp-text 文本替换(经钩子 `tp--tp-text-handler-function` 安装到 tp-ops) | -| `tp-add` | tp-ops.el | 深度合并属性 | -| `tp-push-layer` | tp-stack.el | 推送层到栈顶 | -| `tp-put-layer` | tp-stack.el | 在指定位置放置层 | -| `define-tp` | tp-layer.el | 定义自定义层(宏)| -| `define-tps` | tp-layer.el | 定义层组(宏)| -| `tp--reactive-variable-watcher` | tp-reactive.el | 响应式变量监听器回调 | -| `tp--update-layer-regions` | tp-render.el | 更新使用层的文本区域 | -| `tp-search-map` | tp-search.el | 搜索并应用函数 | -| `tp--match-apply` | tp-search.el | 模式匹配内部实现 | - ---- - -### 2. 深度合并属性 tp-add - -`tp-add` 实现属性的深度合并,特别是 face 属性的智能合并。 - -#### 调用堆栈 - -``` -tp-add (用户调用入口) - │ - ├─→ tp--parse-args (解析参数) - │ - ├─→ tp--handle-tp-text-property (处理 tp-text) - │ - ├─→ text-properties-at (获取现有属性) - │ - ├─→ tp--prepend-face (智能合并 face 属性) - │ │ - │ └─→ tp--deep-merge-plist (递归合并 plist) - │ - ├─→ tp--deep-merge-plist (合并其他嵌套属性) - │ - └─→ put-text-property (设置合并后的属性) -``` - -#### Face 合并逻辑 - -```elisp -;; 输入 -(tp-add 1 10 '(face (:foreground "red"))) ; 已存在 -(tp-add 1 10 '(face bold)) ; 新增 - -;; 结果: face 是 (bold (:foreground "red")) -;; - 符号 face 被前置到列表 -;; - plist face 被深度合并 -``` - ---- - -### 3. 属性层推送 tp-push-layer - -`tp-push-layer` 将属性层推送到栈顶,实现多层属性的堆叠管理。 - -#### 调用堆栈 - -``` -tp-push-layer (用户调用入口) - │ - └─→ tp-put-layer (在指定索引放置层) - │ - ├─→ tp--normalize-layer-spec (规范化层规格) - │ │ - │ └─→ tp-layer-props (获取层属性,添加 tp-name) - │ - ├─→ tp-group-props (如果是层组,获取所有层属性) - │ - ├─→ tp-empty-p (检查是否为空) - │ - ├─→ tp--get-layer-stack (获取现有层栈) - │ - ├─→ tp--build-layer-props (构建包含 tp-layers 的属性) - │ - └─→ tp-intervals-map (遍历区间应用属性) - │ - └─→ set-text-properties / put-text-property -``` - -#### 层栈存储结构 - -```elisp -;; 可见层属性直接存储为文本属性 -;; 隐藏层存储在 tp-layers 属性中 - -;; 例如:两层栈 -'(face (:foreground "red") ; 顶层(可见)属性 - tp-name layer1 ; 顶层名称 - tp-layers ; 下层列表 - ((face (:background "blue") ; 第一个隐藏层 - tp-name layer2))) -``` - ---- - -### 4. 响应式层定义 define-tp - -`define-tp` 宏定义支持响应式变量的自定义层。 - -#### 宏展开流程 - -``` -define-tp (宏调用) - │ - ├─→ tp--parse-define-layer-args (解析 :props, :data, :compute, :watch, :transform) - │ - ├─→ tp--collect-reactive-symbols (收集所有 $var 符号) - │ - ├─→ tp--unregister-reactive-deps (如果重新定义,先清除旧依赖) - │ - ├─→ tp--ensure-reactive-variables (确保 $var 对应的变量已定义) - │ - ├─→ tp--register-layer-data (注册 :data 变量) - │ │ - │ └─→ add-variable-watcher (为每个变量添加监听器) - │ - ├─→ tp--register-layer-computed (注册 :compute 计算属性) - │ - ├─→ tp--apply-initial-computed (计算初始值) - │ - ├─→ tp--register-reactive-deps (注册响应式依赖) - │ - ├─→ tp--register-layer-watchers (注册 :watch 回调) - │ - ├─→ tp--resolve-reactive-symbols (解析 $var 为当前值) - │ - └─→ tp--set-layer-props (存储到 tp-layer-alist) -``` - -#### 关键数据结构 - -```elisp -;; 层定义存储 -tp-layer-alist -;; => ((layer-name arglist body-form) ...) -;; 或 ((layer-name nil resolved-props) ...) ; 非参数化层 - -;; 响应式依赖 -tp-reactive-deps -;; => ((my-color . ((my-layer . '(face (:foreground $my-color))))) -;; (my-bg . ((my-layer . '(face (:background $my-bg)))))) - -;; 计算属性 -tp-layer-computed -;; => ((my-layer . ((full-name . (lambda () (concat first last)))))) - -;; 数据变量 -tp-layer-data -;; => ((my-layer . (first-name last-name))) - -;; 监听回调 -tp-layer-watchers -;; => ((my-layer . ((first-name . (lambda (new old layer) ...))))) - -;; 转换函数 -tp-layer-transforms -;; => ((my-layer . (lambda (text) (upcase text)))) -``` - ---- - -### 5. 响应式更新触发 - -当响应式变量通过 `setq` 改变时,自动触发更新。 - -#### 调用堆栈 - -``` -(setq my-color "blue") ; 用户改变变量 - │ - └─→ tp--reactive-variable-watcher (由 add-variable-watcher 注册) - │ - ├─→ (检查是否在批量更新模式) - │ └─→ 如果是,添加到 tp--batch-update-pending 并返回 - │ - ├─→ tp--invoke-layer-watchers (调用 :watch 回调) - │ - ├─→ tp--update-layer-computed (更新计算属性) - │ │ - │ ├─→ (调用计算函数) - │ ├─→ tp--resolve-reactive-symbols (解析新值) - │ └─→ tp--set-layer-props (更新层定义) - │ - ├─→ tp--update-layer-regions (更新文本区域) - │ │ - │ ├─→ tp-layer-props (获取新属性) - │ │ - │ └─→ tp-search-map (遍历所有使用该层的区域) - │ │ - │ └─→ tp-add (合并新属性) - │ - └─→ tp--update-reactive-text (如果 tp-text 依赖该变量) - │ - └─→ tp--replace-reactive-text-in-buffer - │ - └─→ (删除旧文本,插入新文本) -``` - -#### 批量更新优化 - -```elisp -(tp-with-batch-updates - (setq my-color "red") ; 不立即更新 - (setq my-bg "blue")) ; 不立即更新 -;; 退出时一次性更新所有变化 - -;; 内部实现: -;; 1. 设置 tp--batch-update-active = t -;; 2. 变量变化被记录到 tp--batch-update-pending -;; 3. 退出时调用 tp--flush-batch-updates -``` - ---- - -### 6. 模式匹配 tp-match-set - -`tp-match-set` 在字符串匹配处设置属性。 - -#### 调用堆栈 - -``` -tp-match-set (用户调用入口) - │ - └─→ tp--match-apply (内部实现) - │ - ├─→ (如果 OBJECT 是字符串,copy-sequence 创建副本) - │ - ├─→ (循环搜索 PATTERN) - │ │ - │ ├─→ search-forward / string-match - │ │ - │ └─→ tp-set (在匹配区域设置属性) - │ - └─→ (返回匹配区域列表或带属性字符串) -``` - -#### 支持多模式 - -```elisp -(tp-match-set '("TODO" "FIXME") '(face warning)) -;; => 匹配所有 TODO 和 FIXME -``` - ---- - -### 7. 搜索与遍历 tp-search-map - -`tp-search-map` 对所有匹配属性的区域应用转换函数。 - -#### 调用堆栈 - -``` -tp-search-map (用户调用入口) - │ - └─→ tp--search-do (内部搜索实现) - │ - ├─→ (区分字符串和缓冲区处理) - │ - ├─→ (对于字符串) - │ │ - │ ├─→ text-property-search-forward (搜索) - │ │ - │ └─→ (调用 FUNCTION,传入 text, start, end, idx) - │ │ - │ └─→ (替换匹配文本为函数返回值) - │ - └─→ (对于缓冲区) - │ - ├─→ text-property-search-forward - │ - └─→ (相同处理,但修改缓冲区) -``` - -#### 函数签名 - -```elisp -(tp-search-map - (lambda (text &optional start end idx) - (upcase text)) ; 返回值替换原文本 - 'marker ; 搜索的属性名 - nil ; 值(nil 表示不匹配值) - my-string ; 目标对象 - 0 ; 起始位置(可选) - 100) ; 结束位置(可选) -``` - ---- - -## 关键数据结构 - -### 1. tp-layer-alist - -存储所有层定义。 - -```elisp -;; 非参数化层 -((highlight nil '(face (:background "yellow"))) - (error nil '(face (:foreground "red")))) - -;; 参数化层 -((tp-space (pixel) `(display (space :width (,pixel))))) -``` - -### 2. tp-layer-groups - -存储层组定义。 - -```elisp -((status-colors . (highlight error info)) - (moon-phases . (moon-phases-new moon-phases-full))) -``` - -### 3. tp-reactive-deps - -存储响应式依赖关系。 - -```elisp -;; 变量 -> ((层名 . 使用该变量的属性列表) ...) -((my-color . ((my-layer . '(face (:foreground $my-color))) - (other-layer . '(face (:background $my-color))))) - (my-size . ((size-layer . '(display (space :width $my-size)))))) -``` - -### 4. 层栈结构 - -文本区域的层栈通过 `tp-name` 和 `tp-layers` 属性存储。 - -```elisp -;; 位置 1-10 的属性 -'(face (:foreground "red") ; 可见属性 - tp-name layer1 ; 顶层名称 - help-echo "tip" ; 可见属性 - tp-layers ; 隐藏层列表 - ((face (:background "blue") tp-name layer2) - (face (:underline t) tp-name layer3))) -``` - ---- - -## 潜在问题分析 - -### 1. 性能问题 - -#### 问题 1.1:响应式更新可能导致性能瓶颈 - -**现象**:当一个响应式变量被多个层使用,且这些层被应用到大量文本区域时,变量变化会触发大量更新。 - -**代码位置**:`tp--update-layer-regions` 函数 - -**问题代码**: -```elisp -(defun tp--update-layer-regions (var layer-name) - ;; 遍历所有缓冲区 - (dolist (buf (buffer-list)) - ;; 在每个缓冲区中搜索所有使用该层的区域 - (tp-search-map (lambda (txt) ...) 'tp-name layer-name nil buf))) -``` - -**建议**: -- 添加缓冲区级别的响应式依赖跟踪 -- 只更新实际使用该层的缓冲区 -- 考虑使用惰性更新策略 - -#### 问题 1.2:tp-intervals 可能在大文件中变慢 - -**现象**:`object-intervals` 返回整个对象的所有区间,然后过滤。 - -**建议**:对于大文件,考虑使用 `next-property-change` 进行增量遍历。 - ---- - -### 2. 内存问题 - -#### 问题 2.1:匿名层名称无限增长 - -**现象**:`tp--anonymous-layer-counter` 只增不减,匿名层名称永不重用。 - -**代码位置**:`tp--generate-anonymous-layer-name` - -**建议**: -- 使用弱引用跟踪匿名层 -- 当层不再被使用时自动清理 - -#### 问题 2.2:响应式依赖可能泄漏 - -**现象**:如果缓冲区被杀死,但响应式依赖未清理,可能导致内存泄漏。 - -**建议**: -- 添加 `kill-buffer-hook` 来清理缓冲区相关的依赖 -- 定期检查并清理无效依赖 - ---- - -### 3. 功能问题 - -#### 问题 3.1:层栈深度无限制 - -**现象**:没有限制层栈的最大深度,可能导致性能问题。 - -**建议**:添加可配置的最大深度限制。 - -#### 问题 3.2:参数化层的参数验证不足 - -**现象**:参数化层不验证传入参数的类型和数量。 - -```elisp -(define-tp tp-space (pixel) - `(display (space :width (,pixel)))) - -;; 错误调用不会报错 -(tp-set "test" 'tp-space) ; 缺少参数 -``` - -**建议**:在层调用时添加参数验证。 - ---- - -### 4. 代码质量问题 - -#### 问题 4.1:部分函数过长 - -**现象**:某些函数超过 100 行,如 `tp--parse-args`、`tp-set`。 - -**建议**:拆分为更小的辅助函数。 - -#### 问题 4.2:文档字符串不一致 - -**现象**:部分内部函数缺少文档字符串。 - -**建议**:为所有公开函数和重要内部函数添加文档。 - -#### 问题 4.3:错误处理不完善 - -**现象**:某些边界情况没有清晰的错误消息。 - -```elisp -;; 调用未定义的层 -(tp-set 1 10 'undefined-layer) -;; 可能静默失败或产生不明确的错误 -``` - -**建议**:添加清晰的错误检查和消息。 - ---- - -### 5. 测试覆盖问题 - -#### 问题 5.1:某些边界情况未测试 - -**现象**:测试主要覆盖正常流程,边界情况覆盖不足。 - -**需要补充的测试**: -- 空字符串/空缓冲区处理 -- 极端层栈深度 -- 循环依赖检测 -- 并发修改场景 - ---- - -## 架构优化建议 - -### 1. 短期优化(低成本高收益) - -#### 1.1 添加错误边界 - -```elisp -(defun tp-set (...) - "..." - (condition-case err - (tp--set-internal ...) - (error - (tp-debug-log "Error in tp-set: %s" err) - (signal (car err) (cdr err))))) -``` - -#### 1.2 添加性能日志 - -```elisp -(defmacro tp--with-timing (name &rest body) - "Execute BODY and log timing if tp-debug-mode is enabled." - `(let ((start (float-time))) - (prog1 (progn ,@body) - (when tp-debug-mode - (tp-debug-log "%s took %.3fms" - ,name - (* 1000 (- (float-time) start))))))) -``` - -#### 1.3 参数验证 - -```elisp -(defun tp-layer-props (name &optional include-tp-name) - "Get properties for layer NAME." - (unless (symbolp name) - (error "Layer name must be a symbol: %S" name)) - ...) -``` - -### 2. 中期优化(改进用户体验) - -#### 2.1 层定义的本地化 - -当前所有层定义是全局的。考虑支持缓冲区本地层: - -```elisp -(define-tp-local my-local-layer () - '(face bold)) -``` - -#### 2.2 层的命名空间 - -避免层名冲突: - -```elisp -(define-tp (my-package . highlight) () - '(face (:background "yellow"))) -``` - -#### 2.3 属性继承 - -允许层继承其他层: - -```elisp -(define-tp error-highlight () - :inherit 'base-highlight - :props '(face (:foreground "red"))) -``` - -### 3. 长期优化(架构改进) - -#### 3.1 响应式系统优化 - -考虑采用脏标记 + 批量更新模式: - -```elisp -;; 标记脏数据 -(defvar tp--dirty-layers nil) - -;; 在 idle 时更新 -(run-with-idle-timer 0.1 t #'tp--flush-dirty-layers) -``` - -#### 3.2 层栈优化 - -使用更高效的数据结构: - -```elisp -;; 当前:列表 -tp-layers -> ((props1) (props2) (props3)) - -;; 优化:使用向量 -tp-layers -> [props1 props2 props3] -``` - -#### 3.3 增量更新 - -对于大区域,考虑增量更新: - -```elisp -(defun tp--update-region-incremental (start end new-props) - "Update region incrementally using property change boundaries." - (let ((pos start)) - (while (< pos end) - (let ((next (next-single-property-change pos 'tp-name nil end))) - (tp--update-single-interval pos next new-props) - (setq pos next))))) -``` - ---- - -## 开发入门指南 - -### 1. 开发环境设置 - -```elisp -;; 加载开发版本 -(add-to-list 'load-path "/path/to/tp") -(require 'tp) - -;; 启用调试模式 -(setq tp-debug-mode t) -(setq tp-debug-echo t) - -;; 运行测试(全套 439 个 ERT 测试) -;; make test -``` - -### 2. 添加新功能的步骤 - -1. **理解分层架构** - - 确定新功能属于哪个模块(见 [ARCHITECTURE.md](ARCHITECTURE.md)) - - 遵循模块间调用规则(只调用前置模块的函数) - -2. **编写测试用例** - - 在 `tests/` 下对应模块的 `tp-*-tests.el`(或综合套件 `tp-tests.el`)中添加测试 - - 覆盖正常流程和边界情况 - -3. **实现功能** - - 添加必要的辅助函数 - - 添加文档字符串 - - 处理错误情况 - -4. **更新文档** - - 更新 README.md / README_CN.md - - 如果涉及架构变化,更新 ARCHITECTURE.md - -### 3. 调试技巧 - -```elisp -;; 查看层定义 -tp-layer-alist - -;; 查看响应式依赖 -tp-reactive-deps - -;; 查看位置属性 -(tp-at 5) -(tp-at 5 'face) -(tp-at 5 '(face :foreground)) - -;; 查看区间 -(tp-intervals 1 100) - -;; 查看调试日志 -(tp-debug-show) -``` - -### 4. 常见开发任务 - -#### 添加新的核心属性函数 - -1. 在 `tp-ops.el` 添加函数 -2. 使用 `tp--parse-args` 解析参数 -3. 调用 Emacs 原生 API -4. 添加测试用例 - -#### 添加新的层栈操作函数 - -1. 在 `tp-stack.el` 添加函数 -2. 使用 `tp--stack-map-region` 遍历区域内层栈 -3. 使用 `tp--get-layer-stack` 获取层栈 -4. 添加测试用例 - -#### 扩展响应式系统 - -1. 注册/监听逻辑放在 `tp-reactive.el`,渲染逻辑放在 `tp-render.el` -2. 使用 `add-variable-watcher` 注册监听 -3. 在适当位置调用 `tp--update-layer-regions`(下层模块经钩子变量触发) -4. 添加测试用例 - ---- - -## 总结 - -tp.el 是一个设计精良的文本属性操作库,其核心创新包括: - -1. **统一的 API 设计**:同一函数支持多种调用方式 -2. **属性层系统**:实现了类似图层的属性管理 -3. **响应式更新**:借鉴前端框架思想,实现数据驱动 UI - -主要的改进方向: - -1. **性能优化**:响应式更新的效率、大文件处理 -2. **错误处理**:参数验证、清晰的错误消息 -3. **代码质量**:函数拆分、文档完善、测试覆盖 - -对于想要参与开发的贡献者,建议: - -1. 从理解测试用例开始 -2. 使用调试模式跟踪执行流程 -3. 遵循分层架构原则 -4. 先写测试,后写实现 - ---- - -*报告生成时间: 2026-01-10(分析对象:拆分前的单文件 tp.el 0.1.0)* -*文件结构与"关键代码位置"表更新于 2026-07-26(tp 0.2.0 模块化后)* +- [TP 1.0 公共 API 参考](API-REFERENCE.md) +- [API 语义合同](API-SEMANTICS.md) +- [当前架构](ARCHITECTURE.md) +- [README](../README_CN.md) diff --git a/docs/REPOSITORY-AUDIT.md b/docs/REPOSITORY-AUDIT.md index 1ec1566..c217ee5 100644 --- a/docs/REPOSITORY-AUDIT.md +++ b/docs/REPOSITORY-AUDIT.md @@ -1,1899 +1,11 @@ -# tp 仓库系统审计与“文本属性操作替代层”能力评估 +# Repository audit(历史归档) -> **历史 TP 0.3 审计,TP 1.0 已废弃。** 本文固定记录提交 `a65d799` 的审计证据和当时完成的 0.3 路线,不描述 TP 1.0 当前架构。下文的 managed layer/stack、`tp-render.el`、`tp-stack.el`、`tp-text`、inline `tp-name`/`tp-layers`/`tp-meta`、扫描刷新、阶段状态和代码链接均应按历史快照阅读,不应作为现行 API 或实现依据;正文语义保持原样。当前事实见 [README](../README_CN.md)、[当前架构](ARCHITECTURE.md)、[API 合同](API-SEMANTICS.md) 与 [1.0 变更记录](../CHANGELOG.md)。 +这是 TP 0.3 的仓库审计快照,不是当前实现的功能清单。快照中的旧 stack、 +renderer、tp-text、扫描刷新和 registry 结论不能用于 TP 1.0。 -> 审计日期:2026-07-28 -> 审计快照:`a65d799`(tp 0.3.0) -> 结论置信度:高 -> 范围:公开 API 语义、核心实现、层栈与响应式设计、Emacs 原生语义覆盖、 -> 测试与 CI、性能风险、文档一致性,以及后续扩展路线。 +当前仓库提供的功能和用法以以下文档及 source 为准: -> 实施状态(2026-07-28):阶段 0 语义已冻结在 -> [API-SEMANTICS.md](API-SEMANTICS.md);阶段 1 的 TP-A01~A06、TP-A10 与 -> TP-A11 已按本文路线实现回归测试和修复;阶段 2 canonical façade 已完成为 -> 内部记录/数据流模型,公开返回保持兼容;阶段 3 text-only 原生语义 façade -> 已完成;阶段 4 managed lifecycle 已完成;阶段 5 overlay-aware 查询已完成, -> overlay lifecycle 明确排除。下文的问题描述保留为审计快照证据,不代表 -> 修复后工作树的现状。 - -> 当前验收结论:按路线编号共有阶段 0~5(6 个阶段;若只统计开发阶段则为 -> 1~5 共 5 个),现已全部完成。tp 已能作为日常文本属性操作的统一高层 -> façade,并对 direct/effective/source、显式 nil、change/predicate、 -> mutation policy、managed lifecycle 和 overlay-aware lookup 给出公开契约。 -> “替代”不表示重写 Emacs 引擎:overlay lifecycle、yank/stickiness/undo -> 等底层机制继续显式委托原生 API。 - -## 0. 结论先行 - -### 0.1 直接回答 - -如果“替代所有文本属性操作”是指: - -1. 用更统一、易记的接口完成日常的属性设置、查询、删除、批量匹配; -2. 同时提供原生 API 没有的命名层、层栈、响应式属性、调色板等高层能力; - -那么 tp **已经基本具备成为首选高层工具箱的条件**。 - -如果“替代”是指: - -1. tp 的每个 API 在字符串与缓冲区上都具有稳定、可预测、统一的语义; -2. 可以覆盖或等价映射 GNU Emacs 的全部文本属性读取、写入、边界搜索、 - 插入继承、分类默认值、撤销、复制/yank、特殊属性行为; -3. 进一步覆盖 `get-char-property` 所代表的文本属性与 overlay 联合视图; -4. 现有用户可以不再理解 Emacs 原生语义而只依赖 tp; - -那么答案是 **目前不能**。 - -当前最准确的定位应当是: - -> **tp 是建立在 Emacs 原生文本属性之上的高层操作工具箱,并额外提供一套 -> 受管理的命名层、层栈与响应式渲染模型。** - -不宜在当前版本中把它描述为: - -> Emacs 全部文本属性/字符属性操作的语义等价替代品。 - -### 0.2 为什么还不能称为“完整替代” - -主要原因不是函数数量不足,而是以下四个语义问题: - -1. **公共 API 契约尚未完全收敛。** 同一函数在字符串和缓冲区上可能采用不同 - 的修改方式、坐标、返回类型与搜索结果结构。 -2. **“属性不存在”和“属性存在但值为 nil”没有被全程区分。** 这会直接破坏 - 精确查询、搜索、覆盖和删除语义。 -3. **原生文本属性模型与 tp 的受管理层模型有两个不同的所有权系统。** - `tp-set` 展开命名层后通常不保留身份,而 `tp-push-layer`/`tp-put-layer` - 会写入 `tp-name`/`tp-layers`;二者不能互换。 -4. **Emacs 的完整语义远大于“给区间写 plist”。** 分类默认值、属性别名、 - stickiness、插入继承、撤销、yank 过滤、special properties、overlay-aware - 查询等仍没有成为 tp 的明确 API 契约。 - -此外,本次审计确认了数个当前测试未覆盖的实现问题,其中至少两个会造成用户 -可见的属性错误,见[第 6 节](#6-已确认的问题与风险排序)。 - -### 0.3 三个替代层级 - -建议把“替代”拆成三个可验证层级,而不是使用一个无法验收的口号: - -| 层级 | 定义 | 当前状态 | -| --- | --- | --- | -| A. 日常操作替代 | 常见设置、读取、删除、匹配、遍历可优先使用 tp | **较强,接近可用** | -| B. 原生文本属性 API 语义替代 | 对原生读取、写入、搜索、复制、nil 值、坐标和返回值有明确等价契约 | **text-only 已达到,公开返回仍兼容历史** | -| C. 完整字符属性生态替代 | 明确覆盖 category/default/alias、stickiness、特殊属性、yank、undo、overlay-aware 查询等 | **查询边界已达到;overlay lifecycle 明确排除** | - -合理的近期目标是先完整达到 B;C 应当被设计成显式兼容层,而不是重新实现 -Emacs 的底层属性引擎。 - ---- - -## 1. 审计方法与证据边界 - -本报告使用了四类证据: - -1. **源码审计**:逐模块检查公开入口、解析器、属性合并、层栈编解码、响应式 - 注册与刷新路径。 -2. **文档交叉检查**:比较 README、函数 docstring、架构文档和 CHANGELOG。 -3. **动态验证**:运行完整编译、ERT、doctest、随机顺序测试,并对关键语义 - 编写最小批处理复现。 -4. **官方语义基线**:以 GNU Emacs Lisp Reference Manual 的 - [Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Text-Properties.html)、 - [Examining Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Examining-Properties.html)、 - [Changing Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Changing-Properties.html)、 - [Property Search](https://www.gnu.org/software/emacs/manual/html_node/elisp/Property-Search.html)、 - [Sticky Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sticky-Properties.html) - 和 [Special Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Special-Properties.html) - 为比较基线。 - -报告使用以下证据标记: - -| 标记 | 含义 | -| --- | --- | -| 已确认 | 有源码路径和动态复现,或有明确测试证明 | -| 高置信推断 | 源码控制流明确,但尚未加入永久回归测试 | -| 需阈值 | 已有 benchmark 基线,但尚未固化为发布阈值 | - -本报告没有把“测试全绿”等同于“没有问题”。现有测试主要证明已经写入测试的 -行为;本次发现的问题恰好说明,缺少跨 API 等价性和状态空间测试时,固定回归 -测试无法证明语义完备性。 - ---- - -## 2. 仓库现状 - -### 2.1 模块结构 - -当前代码采用线性模块边界: - -```text -tp-core - → tp-reactive - → tp-layer - → tp-ops - → tp-search - → tp-render - → tp-stack - → tp-palette - → tp-builtins - → tp.el -``` - -职责总体清晰: - -| 模块 | 当前职责 | 审计评价 | -| --- | --- | --- | -| `tp-core.el` | 区间、plist/face 合并、底层工具 | 边界清晰,但部分“便利查询”会丢失位置信息 | -| `tp-reactive.el` | 依赖、watcher、批量队列、缓冲区注册表 | 机制完整,但生命周期依赖所有写入都经过登记入口 | -| `tp-layer.el` | 层定义、解析、展开、栈存储编码 | 是语义复杂度最高的所有者,直接应用与受管理应用混在同一解析体系 | -| `tp-ops.el` | 核心增删改查、`tp-text` | 重载较多;字符串/缓冲区契约未完全统一 | -| `tp-search.el` | 模式、搜索、导航、替换 | 功能丰富,但返回模型与 nil 查询语义分裂 | -| `tp-render.el` | 响应式重渲染、最小文本更新 | 已有差异更新意识,但层重新定义的 old/new 所有权不足 | -| `tp-stack.el` | 命名层栈操作 | 特性强;本质上是独立于原生 plist 的受管理状态机 | -| `tp-query.el` | 原生文本 lookup/change 封装、修改策略 | text-only Stage 3 与 overlay-aware Stage 5 查询已完成 | -| `tp-palette.el` | 主题色数据 | 独立性好;主题变化后的已应用结果没有完整刷新协议 | -| `tp-builtins.el` | 内置层和显示辅助 | 适合作为上层可选能力,不应决定核心属性语义 | - -模块化本身是成功的。当前最大的架构问题已经不再是“文件太大”,而是: - -> **原生属性操作、层定义展开、受管理层栈、响应式重渲染在 API 层共享了过多 -> 隐式规则,却没有共享一个完整的语义规范。** - -继续拆文件不会自动解决这个问题;首先应收敛状态所有权和公开契约。 - -### 2.2 当前质量基线 - -| 项目 | 当前结果 | -| --- | --- | -| 包版本 | 0.3.0 | -| Emacs 基线 | 28.1+ | -| CI 矩阵 | 28.1 / 29.4 / 30.1 | -| ERT | 636 个,全部通过 | -| doctest | 92 个断言,全部通过 | -| 随机顺序 ERT | 636 个,全部通过;本次种子 `747555` | -| 字节编译 | warning-as-error 下通过 | - -这说明仓库已经有良好的维护基础,尤其是模块加载、固定回归、文档示例和多版本 -兼容性方面。问题主要集中在**未被现有用例枚举到的语义组合**,不是基础工程 -完全缺失。 - ---- - -## 3. 设计上做得好的部分 - -### 3.1 `set` / `reset` / `add` 给出了有价值的高层词汇 - -README 把三者区分为: - -- `tp-set`:只覆盖指定键,保留其他键; -- `tp-reset`:完整替换区间属性; -- `tp-add`:对 face 和嵌套 plist 做合并。 - -这个词汇比直接记忆多个原生函数更容易使用,也确实表达了三个常见意图。 -尤其 `tp-add` 对 face 合并的封装,是原生 API 之上的真实增值,不是简单改名。 - -### 3.2 字符串和缓冲区共用同一组概念 - -核心 API 能处理字符串、当前缓冲区和显式缓冲区,模式/正则/搜索/层栈也尽量 -保持两类对象都可用。这是合理的产品方向。问题不是“应否统一”,而是当前统一 -只发生在函数名层面,坐标、修改性和返回值尚未完全统一。 - -### 3.3 层栈解决了原生文本属性缺少来源身份的问题 - -原生文本属性只保存当前每个字符的 plist,不记录“这个 face 是哪个业务层写入 -的”。tp 用 `tp-name` 和 `tp-layers` 保存来源与顺序,使以下操作成为可能: - -- 按名称删除某一层,而不是猜测具体属性值; -- 上移、下移、旋转、隐藏、显示; -- 保留下层并只渲染当前顶层; -- 更新被覆盖或隐藏的响应式层; -- 将多个业务状态组合在一个文本范围上。 - -这是仓库最有差异化的能力,应继续作为核心卖点。 - -### 3.4 响应式系统已经考虑了增量更新和生命周期 - -当前实现有: - -- 变量 watcher; -- 依赖注册; -- 层到缓冲区的索引; -- buffer-local 值; -- 批量更新; -- 已隐藏/埋藏层的写穿; -- `tp-text` 的最小编辑更新; -- 匿名层回收; -- 对“插入已带属性字符串绕过登记”的已知缺口给出手工跟踪入口。 - -这不是一个简单 demo。不过,这也意味着它已经是一套小型渲染系统,需要以 -状态机和生命周期的标准来测试,而不能只按几个属性函数来测试。 - -### 3.5 CI 和文档覆盖明显高于一般小型 Elisp 库 - -多版本编译、warning-as-error、完整 ERT、随机顺序和 doctest 都是有效资产。 -`docs/ARCHITECTURE.md` 也对模块职责和反向钩子边界做了明确说明。后续不应推翻 -这些基础,而应补充“语义规范”和“等价性测试”两块缺失拼图。 - ---- - -## 4. “完整替代”所需的 Emacs 语义基线 - -### 4.1 原生模型不是稳定的 interval 对象模型 - -Emacs 的基本模型是: - -- 字符串或缓冲区中的每个字符携带一个属性 plist; -- `[START, END)` 是操作范围; -- property change functions 从值变化推导边界; -- interval 是实现和检查视图,不是用户持有的稳定业务实体。 - -GNU 手册专门说明了 -[Why Text Properties are not Intervals](https://www.gnu.org/software/emacs/manual/html_node/elisp/Not-Intervals.html)。 - -这意味着 tp 可以提供 `tp-intervals` 作为便利视图,但不应把 interval 的分割 -方式当成稳定身份,也不应让公开返回值依赖内部 interval 碎片数量。 - -当前多个层栈修改函数返回“改写的 property run 数量”。这个数会随无关属性 -边界变化而变化,因此更像调试指标,不是稳定的业务结果。 - -### 4.2 读取不是简单的 `plist-get` - -原生读取可能涉及: - -1. 字符上的直接文本属性; -2. `category` 符号 plist 提供的默认值; -3. `default-text-properties`; -4. `char-property-alias-alist`; -5. 对 `get-char-property` 而言,还包括 overlay 与优先级。 - -因此以下三个问题必须明确区分: - -- 该字符是否**直接携带**属性键; -- Emacs 解析后该字符的**有效属性值**是什么; -- 这个值来自文本、category/default/alias,还是 overlay。 - -当前 tp 的特定属性读取会调用 `get-text-property`,因此会**间接继承** -category/default/alias 的有效值解析;而“全部属性”和 presence 查询使用 -`text-properties-at`,只看到直接 plist。问题不是底层能力完全缺失,而是公开 -API 没有明确命名 direct/effective/source 三种视图,也没有相应兼容性测试。 - -### 4.3 nil 是合法值,不等于缺失 - -在 plist 模型中: - -```elisp -(property nil) -``` - -与完全没有 `property` 键不是同一状态。前者可以显式遮蔽较低优先级来源。 - -因此任何完整替代 API 都必须为下列查询提供不同结果: - -1. 缺少键; -2. 存在键且值为 nil; -3. 存在键且值为非 nil; -4. 调用者没有传 VALUE,表示匹配任意值。 - -当前 tp 在若干路径中使用 `plist-get` 的真值或参数 `nil` 同时表达多个状态, -这是最需要优先消除的语义根因之一。 - -### 4.4 写属性会参与修改、撤销和 hook - -GNU Emacs 中,缓冲区文本属性变化通常会: - -- 设置 buffer modified; -- 进入 undo; -- 触发相关 modification hooks; -- 受 `read-only` 和 `inhibit-read-only` 影响。 - -`with-silent-modifications` 可以用于不应污染 modified/undo 的纯属性更新,但它 -有明确边界,不能粗暴包住真实文本修改。 - -tp 当前一些路径尊重原生行为,另一些路径通过 -[`tp-with-current-buffer`](../tp-core.el#L110-L115) 无条件绑定 -`inhibit-read-only`。如果目标是原生语义替代,这种“默认强制写入”必须成为 -显式策略,而不是辅助宏的隐藏副作用。 - -### 4.5 属性会影响未来编辑和交互 - -`front-sticky`、`rear-nonsticky` 与 `text-property-default-nonsticky` 决定新文本 -怎样继承属性;`insert-and-inherit` 与普通 `insert` 又不同。 - -此外,以下属性不是被动数据: - -- `read-only`; -- `modification-hooks`、`insert-in-front-hooks`、`insert-behind-hooks`; -- `invisible`、`display`、`composition`; -- `keymap`、`local-map`、`help-echo`; -- `field`; -- `cursor-intangible`、`cursor-sensor-functions`; -- `yank-handler`。 - -tp 可以通过通用 setter 写入这些属性,实际行为仍由 Emacs 执行;但“可以写入 -一个键”不等于“tp 已经完整建模它的读取、继承、复制、冲突和生命周期语义”。 - -### 4.6 overlay 不是文本属性,但字符属性查询会看到它 - -overlay: - -- 不随复制文本一起复制; -- 属性变化不进入 buffer modified/undo; -- 有独立的优先级、边界和 hook; -- 会被 `get-char-property`、字段和显示相关语义看到。 - -因此建议把目标写清楚: - -- “完整文本属性 façade”可以不提供 overlay 创建/移动 API; -- 但如果承诺替代 `get-char-property` 或“所有字符属性操作”,就必须提供 - overlay-aware 查询模式和明确的来源信息。 - ---- - -## 5. 当前能力矩阵 - -状态说明: - -- **较完整**:当前 API 足以承担主要工作,契约也基本清楚; -- **部分**:有入口,但存在语义缺口或对象间不一致; -- **间接**:可以写入原生键,行为由 Emacs 执行,tp 没有独立建模; -- **缺失**:没有与目标相匹配的公开能力; -- **超出当前边界**:不一定应该实现,但必须在定位中排除。 - -| 语义面 | 当前能力 | 状态 | 主要缺口 | -| --- | --- | --- | --- | -| 任意属性键写入 | `tp-set/reset/add` | 较完整 | 重载、返回值、read-only 策略仍不统一 | -| 单键增改 | `tp-set` | 较完整 | 名字与原生 `set-text-properties` 易混淆 | -| 全 plist 替换 | `tp-reset` | 较完整 | 与 `tp-text` 的属性保留说明需更精确 | -| face/嵌套 plist 合并 | `tp-add` | 较完整 | 自定义合并规则需正式规范化 | -| 单键/多键删除 | `tp-remove`、`tp-clear` | 部分 | 字符串子属性删除可留下“存在但为 nil”的键 | -| 位置读取 | `tp-at`、`tp-member`、`tp-lookup` | 已达到 Stage 3/5 | overlay lifecycle 不属于 lookup | -| 区间读取 | `tp-get`、`tp-intervals` | 部分 | `tp-get` 丢弃 nil 值;坐标模式不统一 | -| 属性汇总 | `tp-plist` | 部分 | last-value-wins,丢失属性对应的区间信息 | -| 属性边界 | `tp-intervals`、内部 map、`tp-property-change` | 已达到 text-only Stage 3 | public interval relative 默认作为兼容例外保留 | -| 搜索相等值 | `tp-forward/backward/search`、`tp-property-any/not-all` | 已达到 text-only Stage 3 | 字符串/缓冲区 public 返回仍保持历史兼容差异 | -| predicate 搜索 | `tp-forward/backward/search` | 已达到 text-only Stage 3 | public 返回仍兼容历史 | -| 模式/正则批量写入 | `tp-match-*`、`tp-regexp-*` | 较完整 | 默认绕过 read-only 的策略需显式化 | -| 字符串复制式操作 | 整串 `tp-set/reset/add` | 较完整 | 与字符串区间原地修改形成两套模型 | -| 字符串长度变化替换 | `tp-text` 可返回新字符串 | 部分 | 初次多 interval 属性合并存在错误 | -| category 默认值 | `tp-lookup :mode :text-source/:text-effective` | 已达到 text-only Stage 3 | overlay lifecycle 不参与 | -| `default-text-properties` | `tp-lookup :mode :text-source/:text-effective` | 已达到 text-only Stage 3 | overlay lifecycle 不参与 | -| property alias | `tp-lookup :mode :text-source/:text-effective` | 已达到 text-only Stage 3 | overlay lifecycle 不参与 | -| text-only / char-property 查询 | `tp-lookup` text/char modes | 已达到 Stage 3/5 | overlay 创建/移动/删除/priority 管理排除 | -| stickiness/插入继承 | 直接委托 Emacs | 已文档化并测试 | 无 tp wrapper | -| buffer modified/undo | `tp-with-mutation-policy` + Emacs 副作用 | 三种组合已文档化并测试 | 三种组合之外不承诺统一策略 | -| silent property update | `tp-with-mutation-policy` | 已达到 Stage 3 | `:silent` + `:respect` 明确拒绝 | -| kill/yank 属性处理 | 直接委托 Emacs | 已文档化并测试 | 无 tp wrapper | -| 特殊显示/交互属性 | 可通用写入,行为委托 Emacs | 已文档化 | 无 source/priority wrapper | -| narrowing | 依赖原生缓冲区 | 已文档化并测试 | - | -| indirect buffer | 依赖原生共享文本 | 已文档化并测试 | - | -| 命名层/层栈 | `define-tp`、stack API、managed lifecycle API | Stage 4 已达到 | compositional layer 仍是可选扩展 | -| 响应式重渲染 | watcher + registry + render + theme generation diagnostics | Stage 4 已达到 | benchmark 基线已记录,非发布阈值 | -| overlay 创建/移动/删除 | 无 | 明确排除 | 使用 Emacs 原生 overlay lifecycle | - -### 5.1 能否设置所有原生属性 - -多数情况下可以通过通用 plist 写入任意属性键,因此 `tp-set` 的表达力不受一个 -固定 allowlist 限制。 - -但 [`tp--builtin-text-properties`](../tp-core.el#L45-L72) 被用于禁止层名与 -原生属性名冲突。这个手工列表: - -- 不可能天然随 Emacs 新版本保持完整; -- 已缺少如 `cursor-sensor-functions` 等官方属性; -- 同时包含 `evaporate` 这类 overlay-only 概念。 - -所以它不应被视为原生属性语义的权威目录。更稳妥的做法是给 tp 层使用独立命名 -空间,或只禁止 tp 自己的保留元数据键;不要试图手工穷举 Emacs 属性名。 - ---- - -## 6. 已确认的问题与风险排序 - -### 6.1 汇总 - -| ID | 优先级 | 问题 | 证据 | 影响 | -| --- | --- | --- | --- | --- | -| TP-A01 | P0 | 初次 `tp-text` 应用会把位置 0 的内嵌属性扩散到后续 interval | 动态复现 + 源码 | 输出属性错误 | -| TP-A02 | P0/P1 | 静态层重定义不刷新;已触发刷新时旧键仍可残留 | 两组动态复现 + 源码 | 已显示内容与定义不一致 | -| TP-A03 | P1 | 显式 nil 无法覆盖 `tp-text` 内嵌属性 | 动态复现 + 源码 | nil 语义错误 | -| TP-A04 | P1 | 搜索 API 把省略 VALUE 与 VALUE=nil 混为一体,且字符串/缓冲区结果不一致 | 动态复现 + 源码 | 无法精确搜索 nil | -| TP-A05 | P1 | 字符串子属性删除可留下 `(PROPERTY nil)`,缓冲区路径则移除键 | 动态复现 + 源码 | 同一 API 两种状态 | -| TP-A06 | P1 | `NOERROR` 捕获所有内部错误 | 动态复现 + 源码 | 真实 bug 被静默吞掉 | -| TP-A07 | P1 | 直接层应用与层栈应用的身份语义分裂 | 源码 + 文档/测试 | 用户难以预测后续层操作 | -| TP-A08 | P1 | 修改性、坐标和返回值在对象/函数族之间不一致 | 源码 + 文档 | 很难成为稳定 façade | -| TP-A09 | P1 | 多处文档与实际实现矛盾 | 源码/docstring | 用户按文档编程会出错 | -| TP-A10 | P1 | 隐藏层存在时,外部直接属性编辑会被后续栈操作静默丢弃 | 动态复现 + 源码明确说明 | 用户属性数据丢失 | -| TP-A11 | P1/P2 | transform/compute 等业务计算失败后 fallback/跳过,observer 错误又无结构化报告 | 源码 | 产生貌似有效但错误或过期的输出 | -| TP-A12 | P2 | 响应式注册依赖入口完整性,插入带属性字符串需手工登记 | 源码明确承认 | 更新可能延迟或漏掉 | -| TP-A13 | P2 | 缺少全矩阵原生等价性和 CI 性能阈值 | 测试审计 | 无法证明“完整替代”与性能 SLA | - -### 6.2 TP-A01:初次 `tp-text` 多 interval 属性扩散 - -**状态:已确认。** - -[`tp--merge-string-props-into-plist`](../tp-core.el#L326-L352) 明确只读取字符串 -位置 0 的属性。初次 `tp-text` 处理路径随后把这一结果作为整个替换区域的基础 -属性;虽然部分路径又按 interval 应用属性,但位置 0 的值已经被提前合入,造成 -后续 interval 污染。 - -最小语义场景: - -```text -FINAL-TEXT: - [0,2) face=bold - [2,4) face=italic -``` - -本次批处理复现中: - -- 字符串初次应用的后半段同时得到 `bold` 与 `italic`; -- 缓冲区初次应用把 `bold` 扩散到整个替换范围。 - -而后续响应式更新已有按 interval 保留属性的专门路径 -([`tp-render.el`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-render.el#L339-L427))。这说明当前同一功能的“初次应用” -和“更新应用”使用了两套不完全一致的合并引擎。 - -现有 [`tp-render-tests.el`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-render-tests.el#L318-L334) 覆盖了后续响应式更新, -没有覆盖初次应用的这个状态。 - -**根因:** - -不是简单漏了一个条件,而是“字符串内嵌属性的所有者”在两个阶段不一致: - -- 一条路径把位置 0 属性提升为全区域属性; -- 另一条路径把内嵌属性视为 per-interval 数据。 - -**修复方向:** - -只保留一个 per-interval 合并入口。禁止任何初次应用路径把 position 0 的属性 -当成整串代表值。修复前先加入字符串与缓冲区两组红色回归测试。 - -### 6.3 TP-A02:层重定义存在两条独立的刷新缺陷 - -**状态:两条均已确认。** - -#### A. 静态层重定义不触发已应用区域刷新 - -静态 simple 层的定义路径只更新 `tp-layer-alist`,没有调用 -`tp--layer-refresh`([`tp-layer.el`](../tp-layer.el#L427-L439))。 - -复现流程: - -```elisp -(tp-layer-reset) -(define-tp audit-static () '(face bold help-echo "old")) - -;; 在临时缓冲区插入 "abc" 并 push audit-static 后: -(define-tp audit-static () '(face italic help-echo "new")) -``` - -实际: - -```text -face=bold -help-echo="old" -stack=((audit-static face bold help-echo "old")) -``` - -预期: - -```text -face=italic -help-echo="new" -stack=((audit-static face italic help-echo "new")) -``` - -因此 `face` 也完全不刷新,这与“旧键残留”不是同一个问题。 - -#### B. 已触发 refresh 时,只更新新键,不删除旧键 - -响应式层重定义会触发 refresh,但 -[`tp--merge-props-into-stack-entry`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-render.el#L142-L152) -只复制旧 entry 后写入新 plist 出现的键; -[`tp--update-layer-regions`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-render.el#L190-L233) -也只遍历新 props 做 `put-text-property`。 - -复现把响应式层从: - -```elisp -(face (:foreground $audit-fg) help-echo "old") -``` - -重定义为: - -```elisp -(face (:background $audit-bg)) -``` - -实际: - -```text -face=(:background "blue") -help-echo="old" -stack=((audit-reactive face (:background "blue") help-echo "old")) -``` - -预期 `help-echo` 被删除。 - -**共同根因:** - -层定义更新没有统一的生命周期协议: - -- 静态定义更新没有进入刷新路径; -- 刷新路径又只持有“新层定义”,没有完整替换“旧层 entry”。 - -**修复方向:** - -所有可刷新 managed layer 都应经过同一 old/new entry 替换路径,再从完整 stack -重建可见 plist。对于直接应用但不保留 `tp-name` 的层,应明确它是一次性模板 -展开,不承诺随定义更新。 - -### 6.4 TP-A03:显式 nil 不能覆盖内嵌属性 - -**状态:已确认。** - -[`tp--merge-embedded-props`](../tp-ops.el#L66-L84) 使用: - -```elisp -(let ((existing (plist-get result key))) - (if existing ... embedded-value)) -``` - -当调用者明确传入 `(custom nil)` 时,`plist-get` 返回 nil,代码把它误判为 -“调用者没有这个键”,最终让内嵌值覆盖显式 nil。 - -本次复现: - -```text -内嵌字符串:custom=embedded -调用属性:custom=nil -期望:custom 键存在,值为 nil -实际:custom=embedded -``` - -**修复方向:** - -凡是判断“键是否由调用者提供”必须使用 `plist-member`;`plist-get` 只用于取值。 -随后应全库搜索相同模式,而不是只修这一处。 - -### 6.5 TP-A04:VALUE=nil 无法表达精确搜索 - -**状态:已确认。** - -[`tp-search`](../tp-search.el#L904-L992) 的条件是: - -```elisp -(or (null value) - (equal prop-val value)) -``` - -因此 nil 同时表示: - -- 调用者省略 VALUE,匹配“属性存在且任意值”; -- 调用者明确传入 nil,匹配“属性存在且值为 nil”。 - -两者无法区分。 - -更严重的是 [`tp-forward`](../tp-search.el#L521-L566): - -- 字符串路径委托给 `tp-search`,nil 是 wildcard; -- 缓冲区路径把 nil 传给 `text-property-search-forward` 并固定 predicate 为 - `equal`,语义不同; -- 字符串返回前 N 个 `(START END VALUE)` 列表; -- 缓冲区返回第 N 次搜索的 `prop-match`。 - -本次动态复现确认,同一份属性布局、同一个 nil 参数,在字符串和缓冲区路径上 -匹配到了不同区间。 - -**修复方向:** - -使用不可与合法属性值冲突的 sentinel 表示“VALUE 未提供/任意值”,例如内部 -`tp--any-value`;公共 API 可以通过显式 keyword 或拆分入口表达。所有对象路径 -应先产生同一种规范化 match 结构,再决定是否移动 point。 - -### 6.6 TP-A05:字符串子属性删除留下 nil 键 - -**状态:已确认。** - -字符串子属性删除路径会用 `plist-put` 把父属性重建为 nil,再以 -`set-text-properties` 写回,因此保留属性键、只把值设为 nil;缓冲区路径则会 -真正删除属性键。 - -本次复现中,同样的子属性删除后: - -- 字符串 `tp-member` 返回 `(face nil)`; -- 缓冲区 `tp-member` 返回 nil。 - -相关实现位于 [`tp-ops.el`](../tp-ops.el#L1050-L1072)。 - -**修复方向:** - -先定义子属性删除后父属性为空时的唯一契约: - -- 若父属性没有剩余有效内容,应移除父键; -- 若业务确实需要显式 nil,应由调用者明确请求,而不是删除操作偶然产生。 - -字符串和缓冲区必须使用同一个纯函数计算“旧值 → 新值/删除标记”,然后各自 -只负责写回。 - -### 6.7 TP-A06:`NOERROR` 吞掉层内部执行错误 - -**状态:已确认。** - -[`tp-put-layer`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-stack.el#L279-L342) 的 `NOERROR` 路径使用宽泛的 -`condition-case nil ... (error ...)`。本次定义一个解析成功但求值时主动报错的 -参数化层后,`NOERROR=t` 返回 nil,内部的真实错误也被吞掉。 - -`NOERROR` 合理的语义应当仅是: - -> 找不到指定层/无法解析用户给出的层名时不报错。 - -它不应当吞掉: - -- 参数化层 body 的 bug; -- 响应式计算错误; -- 非法属性结构; -- 栈编解码不变量破坏; -- 任意其他内部异常。 - -**修复方向:** - -不要围住整个执行路径捕获 `error`。先进行可返回“not found”的窄解析,再让后续 -错误自然传播。 - -### 6.8 TP-A07:命名层有两种不兼容的应用语义 - -**状态:已确认。** - -直接属性操作中的层名解析: - -```elisp -(tp-set object 'my-layer ...) -``` - -通常展开为该层的属性,但不保存 `tp-name` -([`tp-layer.el`](../tp-layer.el#L1353-L1366))。因此它是“一次性模板展开”。 - -层栈操作: - -```elisp -(tp-push-layer object 'my-layer ...) -(tp-put-layer object ... 'my-layer ...) -``` - -会保存 `tp-name` 和 `tp-layers` -([`tp-layer.el`](../tp-layer.el#L1419-L1477)),因此是“受管理实例”。 - -这两种行为各自都可以合理,但使用同一个“应用层”词汇会让用户自然期待: - -- 后续可按层名查询或删除; -- 层重定义会更新已应用区域; -- 响应式系统能找到该层; -- 直接设置与 push 只差栈位置。 - -实际并非如此。 - -**修复方向:** - -正式命名两个概念: - -1. **展开模板**:把层定义解析成普通 plist,不保留身份,不参与生命周期; -2. **挂载层实例**:保存身份、进入层栈、可刷新、可按名操作。 - -关键不是增加包装函数,而是让文档、函数名、返回值和测试始终使用同一词汇。 - -### 6.9 TP-A08:修改性、坐标和返回值没有统一模型 - -**状态:已确认。** - -#### 修改性 - -`tp-set/reset/add`: - -- 整串字符串形式返回新字符串; -- 字符串 region 形式原地修改; -- 缓冲区原地修改。 - -层栈的大多数字符串形式又是原地修改 -([`tp-stack.el`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-stack.el#L494-L524))。 - -这意味着仅从函数名或 OBJECT 类型无法判断是否修改原对象,还必须记住调用形式。 - -#### 返回值 - -当前常见返回值包括: - -- 新字符串; -- 原字符串; -- `(START . END)`; -- 被修改的 property run 数量; -- `prop-match`; -- `(START END VALUE)` 列表; -- nil; -- 层栈查询中的 union、最大深度、首个 top。 - -其中“property run 数量”不是稳定业务语义,因为无关属性边界也能改变 run 数量。 - -#### 坐标 - -字符串通常为 0-based、缓冲区为 1-based,这是原生约定,本身合理;但 -[`tp-intervals`](../tp-core.el#L117-L148) 的缓冲区结果默认又改为相对 START 的 -0-based offset,只有 `ABSOLUTE` 才返回原生坐标。一个用户从查询结果继续调用 -`tp-set` 时容易发生 off-by-one。 - -**修复方向:** - -不必强行让字符串和缓冲区使用同一坐标基数,但必须统一: - -- 默认返回 OBJECT 的原生坐标; -- 相对坐标必须通过显式选项请求; -- 查询返回值使用同一种结构; -- 原地修改与复制式转换必须能从入口名称或显式参数判断; -- 修改函数返回稳定的“是否变化/结果对象”,诊断统计另设调试入口。 - -### 6.10 TP-A09:文档和实现存在明确矛盾 - -**状态:已确认。** - -代表性例子: - -1. [`tp--resolve-props`](../tp-layer.el#L1160-L1205) docstring 说符号层名会包含 - `tp-name`,实现却明确传入 nil,不包含 `tp-name` - ([`tp-layer.el`](../tp-layer.el#L1359-L1366))。 -2. [`define-tp`](../tp-layer.el#L316-L367) 的当前使用示例仍声称直接 `tp-set` - 会得到 `tp-name`,与 README 和当前实现相反。 -3. [`tp-search-map`](../tp-search.el#L1053-L1101) docstring 说字符串长度不同时 - 会截断/部分替换,但实际实现 - [`tp--replace-match-text`](../tp-search.el#L670-L736) 会明确报错。 -4. README 宣称“统一 API 参数规范”,但同一文档同时记录了字符串复制/原地 - 修改和搜索结果结构的差异。 - -**修复方向:** - -先写机器可验证的语义表,再从该表更新 README 与 docstring。避免继续分别修补 -中英文文档和函数说明而没有共同规范。 - -### 6.11 TP-A10:隐藏层状态会静默覆盖外部直接属性编辑 - -**状态:已确认,且内部 docstring 已明确说明。** - -当任意层处于 hidden 状态时,`tp-layers` 保存完整层栈,字符的其他直接属性只是 -临时渲染表面。栈编解码文档明确说明,期间发生的原生或 tp 直接属性编辑会在 -下一次 stack operation 时被丢弃 -([`tp-layer.el`](../tp-layer.el#L1519-L1565))。 - -本次最小复现: - -1. push 一个 `face=bold` 的 managed layer; -2. hide 该层; -3. 直接写入 `help-echo="external"`; -4. show 该层。 - -show 前: - -```text -(help-echo "external" - tp-layers ((tp-hidden t face bold tp-name audit-hidden))) -``` - -show 后: - -```text -(face bold tp-name audit-hidden) -``` - -`help-echo` 无提示地消失。这不是单纯的内部实现细节,而是用户数据所有权冲突, -应当与 correctness 缺陷一起处理,不能推迟到新增高级层功能之后。 - -**修复方向:** - -阶段 0 就必须选定默认规则。最小且安全的默认是:managed hidden range 检测到 -无法归属的直接属性变化时,在下一次栈写入前明确报出冲突;若决定采用 -“收编为匿名层”或“明确覆盖”,也必须由公开契约和回归测试证明,不能静默决定。 - -### 6.12 TP-A11:业务计算与 observer 的错误边界混在一起 - -**状态:源码确认,具体产品策略待定。** - -当前: - -- transform 出错后 message 并返回原文本 - ([`tp-ops.el`](../tp-ops.el#L50-L64)); -- initial compute 出错后 message 并跳过赋值 - ([`tp-reactive.el`](../tp-reactive.el#L352-L368)); -- watcher 出错后 message 并继续 - ([`tp-reactive.el`](../tp-reactive.el#L306-L318))。 - -三者不应使用同一策略: - -- transform/compute 决定业务输出,失败后 fallback 或跳过会留下貌似有效、实际 - 错误或过期的文本,应默认传播到外层错误边界; -- watcher 如果只是 observer,隔离单个 callback 可以合理,但必须留下可查询的 - 结构化失败,而不只是易被忽略的 minibuffer message。 - -**修复方向:** - -先按“计算结果所有者”和“副作用观察者”分类,再收敛错误边界。不要为每个函数 -新增一个静默选项;默认让业务计算失败,顶层批量渲染可以汇总多个错误。 - -### 6.13 TP-A12:响应式注册表依赖入口完整性 - -**状态:源码明确承认。** - -[`tp-reactive-layer-buffers`](../tp-reactive.el#L86-L103) 已记录: - -> 插入一个已经带 `tp-name` 的 propertized string 会绕过 buffer operation 的 -> 登记路径。 - -当前补救是: - -- unknown 时进行学习性全 buffer scan; -- 用户调用 `tp-reactive-track-buffer` 手工登记。 - -这在“高层工具箱”定位下可以接受,但在“所有文本属性操作替代层”定位下不够: -用户可以通过原生 `insert`、substring、kill/yank、间接缓冲区等多种路径让属性 -进入缓冲区,注册表无法假设所有变化都经过 tp。 - -**扩展方向:** - -提供一个明确的 managed mount/attach 生命周期;不要尝试监听所有原生属性变化。 -对于外部插入的属性,提供显式 `track/attach` 和可选的 after-change 集成,并在 -文档中说明成本。 - -### 6.14 TP-A13:测试数量多,但不能证明语义完备 - -**状态:已确认。** - -现有 604 个 ERT 和 92 个 doctest 对固定回归很有价值,但缺少: - -- 与原生函数的等价性测试; -- 随机生成属性 interval 的 property-based 测试; -- `nil`、缺失、默认值、category、alias 的组合; -- 字符串/缓冲区同构场景; -- undo、modified、read-only、hook、stickiness; -- indirect buffer 和 narrowing; -- 将大缓冲区、碎片 interval、深层栈、响应式 fan-out 的性能基准固化为 CI 阈值; -- 主要交互命令的 `call-interactively` 路径; -- 调色板/主题切换后的动态视觉验证。 - -`tests/tp-run-shuffled.el` 只是改变测试顺序,不会生成新的输入状态。它能发现全局状态 -泄漏,但不能替代属性状态空间测试。 - ---- - -## 7. API 语义专项分析 - -### 7.1 `tp-set` 的名称容易与原生 `set-text-properties` 发生反向联想 - -原生: - -- `put-text-property`:单键覆盖; -- `add-text-properties`:添加/覆盖给定键; -- `set-text-properties`:完整替换 plist。 - -tp: - -- `tp-set`:部分覆盖; -- `tp-reset`:完整替换; -- `tp-add`:深度/face 合并。 - -tp 内部体系是自洽的,但熟悉 Emacs 的用户看到 `set` 往往会联想到“完整替换”。 -无需为了原生名字机械改 API,但应在语义规范第一屏给出映射表,并让返回值也 -尽量接近相应原生操作: - -| 用户意图 | tp 当前入口 | 最接近的原生概念 | -| --- | --- | --- | -| 覆盖指定键 | `tp-set` | `add-text-properties` / 多次 `put-text-property` | -| 替换全部属性 | `tp-reset` | `set-text-properties` | -| 按自定义规则合并 | `tp-add` | `add-face-text-property` + 自定义 merge | - -### 7.2 过载解析器降低了可预测性 - -[`tp--parse-args`](../tp-ops.el#L248-L317) 支持多种调用形式,并根据: - -- 第一个参数是 string 还是 number; -- 第二个 symbol 当前是否已注册为层; -- 第三个参数是否非 nil; -- 余下参数是否看似 object; - -推断用户意图。 - -这使调用含义依赖全局层注册状态。例如同一个 symbol 在定义为层前后可能进入 -不同解析分支。参数化层还允许 flat、wrapped args 与 extra plist 组合。 - -这种便利适合 REPL,但不适合作为“完整替代层”的唯一规范入口。 - -建议: - -- 保留便利调用作为外观层; -- 内部和正式规范使用单一的 canonical request; -- 在解析完成后立刻得到明确的 `object/start/end/operation/properties/mutation` - 语义,后续模块不再重新猜测; -- 错误应在解析边界一次性报告,不在深层以 nil 继续。 - -这不要求新增多个文件,也不要求引入新依赖;一个小型、明确的数据结构或参数 -规范即可。 - -### 7.3 查询 API 应区分四种问题 - -当前 `tp-at`、`tp-member`、`tp-get`、`tp-plist`、`tp-search` 混合回答了不同问题: - -1. 某位置的全部直接属性是什么? -2. 某直接属性键是否存在? -3. 某属性的有效值是什么? -4. 哪些连续区域满足某个 predicate? - -建议把这四个问题写成正式语义,再决定是否保留现有函数名。尤其: - -- `tp-get` 不应过滤合法 nil; -- `tp-member` 应只回答 presence,不顺带承担范围读取; -- `tp-plist` 的“跨区间 last-value-wins 汇总”应明确标成 summary,而不是区域的 - 属性真相; -- 搜索结果应包含 range、value,并可选包含 source,不依赖对象类型改变结构。 - -### 7.4 层栈不是原生属性的“自然叠加” - -当前层栈只把顶层可见属性渲染到字符的直接 plist,下层放入 `tp-layers` 存储。 -这意味着它更接近图像编辑器的图层栈,而不是 CSS cascade 或多个独立属性来源 -的逐键组合。 - -例如: - -- 顶层只含 `face`; -- 下层含 `help-echo`; - -当前“只显示顶层”模型下,下层 `help-echo` 不一定继续生效。用户若把“属性层” -理解为“每个键按层优先级组合”,会得到不同预期。 - -建议正式命名两种可能模型: - -1. **exclusive layer**:只有最高可见层的整个 plist 生效;当前模型; -2. **compositional layer**:每个属性键独立按优先级求值。 - -无需立即实现第二种,但必须在文档中把第一种说清。若未来扩展第二种,也应作为 -显式模式,不能悄悄改变现有栈语义。 - -### 7.5 区域查询的标量返回值存在信息损失 - -[`tp-layer-list`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-stack.el#L117-L126) 返回区域中出现过的层名 union; -[`tp-layer-count`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-stack.el#L128-L136) 返回所有 run 的最大深度; -[`tp-layer-top`](https://github.com/Kinneyzhang/tp/blob/a65d799/tp-stack.el#L143-L157) 返回第一个带名字的 top。 - -这些定义不是错误,但函数名看起来像在描述“整个区域的统一状态”。当区域内部 -异质时,调用者无法从标量结果判断: - -- 每个 run 是否相同; -- 某层覆盖全部区域还是只出现一次; -- top 是否统一; -- count 的分布是什么。 - -建议保留便利标量,同时提供或强化 run-aware 查询作为权威入口。标量 docstring -中应直接出现 union/max/first,而不是只写“in region”。 - -### 7.6 错误策略应只有两个边界 - -合理的错误边界: - -1. 参数解析/公共命令边界:把非法用户输入转成清晰错误; -2. 可选的用户 callback 边界:说明 callback 失败是否终止渲染。 - -当前策略混合了: - -- transform 出错后 message 并退回原文本 - ([`tp-ops.el`](../tp-ops.el#L50-L64)); -- watcher 出错后 message 并继续 - ([`tp-reactive.el`](../tp-reactive.el#L306-L318)); -- initial compute 出错后跳过 - ([`tp-reactive.el`](../tp-reactive.el#L352-L368)); -- `NOERROR` 可能吞任意内部错误; -- 普通参数错误则向上传播。 - -建议分别规定: - -- pure computation/transform 失败:默认传播,或由顶层统一汇总; -- watcher 作为副作用 observer:可以隔离,但必须记录结构化失败; -- “not found” 使用普通返回值,不用异常; -- 内部不变量错误永不被 `NOERROR` 吞掉。 - -### 7.7 `tp-text` 跨越了属性、文本替换与响应式状态三个领域 - -`tp-text` 看似一个属性键,实际上可能: - -- 替换真实字符串/缓冲区文本; -- 合并新字符串携带的属性; -- 保留旧范围属性; -- 执行 transform; -- 识别 layer identity; -- 更新响应式变量; -- 触发最小差异编辑。 - -它不是普通 text property,而是一条命令协议。把它与任意 plist 键放在同一个 -解析和合并通道中,容易造成 position 0 采样、属性所有权和重复刷新等问题。 - -建议将 `tp-text` 明确定义为“文本 replacement directive”: - -- 先计算 replacement text; -- 再按 interval 计算其内嵌属性; -- 再执行文本修改; -- 最后应用 layer/props; -- 不把 directive 自身持久化为普通文本属性。 - -公开便利语法可以不变,但内部状态机必须与普通 property merge 分离。 - ---- - -## 8. 架构与状态所有权建议 - -### 8.1 保留现有模块,先确立三个语义域 - -不建议为了本次问题再拆一批 `utils`/adapter 文件。现有模块足以承载下面三个 -清晰语义域: - -#### 域 1:原生属性 façade - -负责: - -- 字符串/缓冲区直接属性读取; -- 区间增删改; -- 搜索与 change boundary; -- 原生坐标、修改、undo/read-only 语义; -- category/default/alias/overlay-aware 的显式查询模式。 - -它不拥有层身份。 - -#### 域 2:受管理层实例 - -负责: - -- layer identity; -- stack entry; -- hide/show/move/merge; -- old/new layer contribution; -- mount/attach/detach; -- 响应式依赖和重渲染。 - -它不应把直接外部属性偷偷吸收到某个层,也不应在下次 stack operation 时静默 -丢弃用户不知道属于谁的属性。 - -#### 域 3:上层表现组件 - -负责: - -- palette; -- builtin layers; -- 主题感知; -- UI 辅助。 - -它可以使用前两个域,但不应让核心包无条件承担全部上层状态和加载成本。 - -### 8.2 明确直接属性与 managed layer 的冲突规则 - -当前 stack codec 文档已经说明:存在隐藏层时,之后的直接原生/tp 属性编辑可能 -在下次栈操作中被丢弃 -([`tp-layer.el`](../tp-layer.el#L1519-L1565))。 - -这是重要的用户数据所有权问题,不能只放在内部 docstring。 - -应选择并文档化一种策略: - -1. **严格模式(推荐默认)**:managed range 上发现无法归属的外部直接修改时, - 栈操作报出冲突; -2. **adopt 模式**:把外部直接属性采集成一个显式匿名层; -3. **overwrite 模式**:明确允许栈状态覆盖外部直接修改。 - -关键是不能静默决定。 - -### 8.3 层刷新必须做完整 entry 替换 - -层实例应保存: - -- identity; -- 当前定义或参数; -- 当前解析后的完整 props; -- hidden 状态; -- 必要时的实例参数/版本。 - -重定义/响应式更新时,先生成新 entry,再用新 entry 替换旧 entry,最后从完整 -stack 重新计算可见属性。这样自然解决旧键残留,无需猜测哪些直接属性属于旧层。 - -### 8.4 不要重新实现 Emacs 属性引擎 - -完整替代的正确方向不是自己模拟: - -- undo; -- stickiness; -- special property behavior; -- overlay priority; -- font-lock; -- field motion。 - -这些应继续交给 Emacs。tp 的职责是: - -- 提供完整、显式、可组合的 façade; -- 不破坏原生副作用; -- 对不支持或不建模的部分给出逃生口; -- 用等价性测试证明委托正确。 - ---- - -## 9. 性能与可扩展性分析 - -### 9.1 当前值得保留的优化 - -- layer → buffer 注册表避免每次都扫描 `buffer-list`; -- unknown 状态与 known-empty 分开; -- kill-buffer 清理; -- property interval 遍历; -- `tp-text` 更新尝试做最小文本差异; -- 批量响应式刷新; -- 避免相同值重复 `put-text-property`。 - -### 9.2 主要性能风险 - -以下风险已有可复现 benchmark 基线,但仍不应把单机耗时直接当成发布阈值: - -| 场景 | 风险来源 | 已记录指标 | -| --- | --- | --- | -| 大缓冲区、高 interval 碎片 | 多次 `next-property-change` 与 plist 复制 | `large-text` 与 `fragmented` rows | -| 层多且频繁 move/hide/show | 每 run 解码、重建完整 stack | `stack-depth` rows | -| 响应式 fan-out | watcher × layer × buffer | `reactive-fanout` rows | -| unknown layer | `buffer-list` 学习性扫描 | 尚未单独拆出 | -| `tp-text` 长文本变化 | 差异计算、删除插入、marker/undo | `large-text` rows | -| 匿名层 | 注册表增长和 GC 扫描 | 尚未单独拆出 | -| palette/theme | 重新解析颜色和重渲染 | `theme-managed-refresh` rows | - -`make benchmark` 在 Emacs 30.2 上完成固定 seeds `1`、`7`、`42`、`747555` -以及可复现 generated seed `8675309`。完整记录见 `docs/BENCHMARKS.md`。 -代表性 seed 42 结果: - -| Scenario | Requested / actual | Operations | Scanned | Changed | Refreshed | Elapsed (s) | -| --- | ---: | ---: | ---: | ---: | ---: | ---: | -| large text | 100,000 | 2 | 100,000 | 100,000 | 0 | 0.000302 | -| large text | 1,000,000 | 2 | 1,000,000 | 1,000,000 | 0 | 0.000318 | -| fragmented intervals | 1,000 | 1 | 1,000 | 500 | 0 | 0.047154 | -| fragmented intervals | 10,000 | 1 | 10,000 | 5,000 | 0 | 0.514766 | -| fragmented intervals | 50,000 | 1 | 50,000 | 25,000 | 0 | 2.769766 | -| stack depth | 1 | 2 | 2,000 | 2,000 | 0 | 0.000892 | -| stack depth | 5 | 6 | 2,000 | 2,000 | 0 | 0.003121 | -| stack depth | 20 | 21 | 2,000 | 2,000 | 0 | 0.012660 | -| stack depth | 50 | 51 | 2,000 | 2,000 | 0 | 0.039089 | -| reactive fan-out | 1 / 1 | 1 | 1 | 1 | 1 | 0.001139 | -| reactive fan-out | 10 / 10 | 1 | 10 | 10 | 10 | 0.008153 | -| reactive fan-out | 100 / 100 | 1 | 100 | 100 | 100 | 0.064352 | -| reactive fan-out | 500 / 200 | 1 | 200 | 200 | 200 | 0.127140 | -| theme refresh | 1 / 1 | 1 | 1,000 | 0 | 1 | 0.001691 | - -### 9.3 返回“修改 run 数”会诱导错误优化 - -run 数受无关属性边界影响,既不是修改字符数,也不是修改业务对象数。如果公开 -返回这个值,用户可能把它当成稳定统计或用于控制流。 - -建议: - -- 公开返回 `changed-p` 或结果对象; -- debug/benchmark API 可返回 `runs-visited/runs-written/chars-covered`; -- 不把实现碎片结构暴露成业务保证。 - -### 9.4 `dash` 依赖可以作为低优先级简化项 - -当前 `dash` 主要用于少量 list 操作,很多可以由 `seq`/`cl-lib` 表达。移除依赖 -可能降低安装摩擦,但不是当前语义问题的根因。 - -只有在: - -- 能减少公开安装复杂度; -- 不引入自制 helper 堆栈; -- 完整测试仍通过; - -时才值得处理。优先级应低于 correctness 和 API contract。 - ---- - -## 10. 测试策略升级 - -### 10.1 第一层:为已确认问题补红色回归 - -必须先加入: - -1. `tp-text` 初次应用保留多个内嵌 property interval; -2. 字符串与缓冲区各一组; -3. 显式 nil 覆盖内嵌非 nil; -4. 静态层重定义触发 managed region 刷新; -5. 已触发刷新时删除旧层属性键; -6. `NOERROR` 只吞 unresolved layer,不吞 layer body error; -7. 字符串和缓冲区子属性删除得到相同 presence; -8. 精确搜索 nil 与 wildcard 是两种调用; -9. hidden managed range 上的外部直接编辑不会静默丢失; -10. transform/compute 与 watcher 分别遵守其错误边界; -11. search-map docstring 与实际长度规则一致。 - -### 10.2 第二层:原生等价性测试 - -对 façade 中承诺等价的操作,使用同一随机输入分别执行: - -```text -native primitive -vs. -tp canonical API -``` - -比较: - -- 文本内容; -- `equal-including-properties`; -- 每个 change boundary; -- property presence 与 value; -- buffer modified; -- undo 后内容和属性; -- point/marker; -- 错误类型。 - -建议覆盖: - -- 空区间、单字符、对象末尾; -- 相邻同值/不同值 interval; -- nil 值与缺失; -- 多种 Lisp 值; -- narrowing; -- read-only; -- sticky boundaries; -- category/default/alias; -- indirect buffer。 - -### 10.3 第三层:状态机测试 managed layers - -随机生成操作序列: - -```text -push → put → hide → redefine → show → move → external edit -→ reactive update → delete → flatten -``` - -每一步验证不变量: - -- 每个 stack entry 身份唯一/顺序正确; -- hidden entry 不泄漏到 rendered plist; -- show 后得到最新定义; -- 删除层不会删除其他来源属性; -- encode/decode round-trip; -- 同一操作重复执行幂等; -- 不相关区域不变化。 - -### 10.4 第四层:性能回归 - -性能基准不应只给一次绝对耗时。建议固定: - -- Emacs 主版本; -- 文本长度; -- property run 数; -- 层数; -- buffer 数; -- 响应式依赖 fan-out; -- 重复次数和 GC 策略。 - -记录斜率和阈值,例如: - -- 10 倍 run 数不应出现 100 倍增长; -- 更新已知单 buffer 层时不得扫描全部 buffer; -- 无变化刷新不得写入属性或改变 modified flag。 - -### 10.5 交互与视觉验证 - -为交互命令增加少量 `call-interactively` 路径,验证 prefix、region、current buffer -和错误消息。palette/display/theme 相关能力应增加 GUI Emacs 的可重复截图或状态 -检查,尤其是主题切换后已应用层是否刷新。 - ---- - -## 11. 分阶段实施路线 - -### 阶段 0:冻结目标语义,不新增大功能 - -产出一份短小、可测试的 `API-SEMANTICS.md`,至少规定: - -- 对象和坐标; -- 字符串复制/原地修改; -- buffer modified/undo/read-only; -- presence 与 nil; -- wildcard sentinel; -- 返回结构; -- 直接模板展开 vs managed mount; -- 错误传播; -- tp-text replacement; -- 外部属性与层栈冲突。 - -停止条件:每个公开核心函数都能映射到该规范中的一条操作语义。 - -### 阶段 1:修复 P0/P1 正确性问题 - -顺序: - -1. 为 TP-A01~A06 和 TP-A10 写失败测试; -2. 合并 `tp-text` 初次/更新的 per-interval 属性算法; -3. 全库用 presence-aware 判断替换真值判断; -4. 引入 internal wildcard sentinel; -5. 层 entry 使用完整替换; -6. 收窄 `NOERROR`; -7. 落实 managed range 外部编辑的唯一默认冲突策略; -8. 区分业务计算失败与 observer 失败; -9. 同步 docstring、README 和 doctest。 - -停止条件:回归测试全绿,现有 604 ERT/92 doctest 不退化。 - -### 阶段 2:建立 canonical façade - -状态:已完成(内部模型)。公开入口和历史返回值保持兼容。 - -不要先追求更多便利重载。先保证内部存在唯一规范路径: - -```text -request - = object - + native range - + operation - + properties/value/predicate - + mutation policy - + error/read-only policy -``` - -现有 `tp-set/reset/add/...` 可以继续作为外观入口,但都解析到同一规范路径。 - -停止条件: - -- [x] 字符串与缓冲区的等价场景只在 I/O 边界分支; -- [x] 查询内部结果结构不再随对象类型改变;公开返回保持历史兼容; -- [x] 内部 canonical range 使用原生坐标;`tp-intervals` / `tp-intervals-map` 的缓冲区 relative 默认是保留的公开兼容例外; -- [x] “是否原地修改”可从公开调用清楚判断。 - -### 阶段 3:补齐原生文本属性语义 - -状态:已完成(text-only)。overlay-aware 字符属性查询见阶段 5。 - -按价值从高到低: - -1. [x] presence-aware exact nil; -2. [x] next/previous single/all property change; -3. [x] `text-property-any` / `not-all` / predicate search 对齐; -4. [x] direct vs effective lookup; -5. [x] category/default/alias 有效值委托成为明确且经过测试的 lookup 契约; -6. [x] read-only、modified、undo 和 silent modification 的三种有效策略; -7. [x] stickiness 与 insert 行为直接委托 Emacs,无 tp wrapper; -8. [x] copy/insert/kill/yank 属性保留与过滤直接委托 Emacs,无 tp wrapper; -9. [x] narrowing 与 indirect buffer 坐标/共享文本行为通过原生等价性测试。 - -停止条件:与选定 GNU Emacs 基线的原生等价性测试通过。 - -### 阶段 4:完善 managed layer 生命周期 - -状态:已完成。 - -建议扩展: - -- [x] 显式 managed layer attach/detach; -- [x] 层实例参数和版本; -- [x] old/new entry 全量替换与参数化 args refresh; -- [x] 已选外部编辑冲突策略在 push/hide/show/merge 全路径保持一致; -- [x] 单 managed layer 使用 `tp-layers` 保存 authoritative `tp-meta`,direct rendered/public stack query 不暴露 `tp-meta`; -- [x] 插入带层字符串后的显式 attach; -- [x] theme generation 与 enable/disable conservative refresh diagnostics; -- [x] registry 诊断与泄漏检查入口; -- [x] layer transaction 的结构化成功/失败和 rollback 报告; -- [ ] compositional layer 作为可选扩展,未纳入当前完成条件。 - -停止条件:随机操作序列下 stack/render/registry 不变量持续成立。 - -### 阶段 5:可选的字符属性兼容层 - -状态:已完成(lookup)。overlay lifecycle 明确排除。 - -若产品确实要使用“所有字符属性操作”定位,再增加: - -- [x] text-only 与 overlay-aware lookup 模式; -- [x] source-aware result,overlay 获胜时报告 `:overlay` 与 overlay identity; -- [x] overlay priority/change boundary 委托 Emacs `get-char-property-and-overlay`; -- [x] fields、buttons、display 等特殊属性行为委托 Emacs,不提供高层 wrapper。 - -不建议把 overlay 的创建/移动/删除硬塞进现有文本属性函数;应保持清晰边界。 - -停止条件:文档明确哪些语义由 tp 提供,哪些委托 Emacs,哪些刻意排除。 - ---- - -## 12. 可拓展方向 - -以下扩展建立在前述 correctness 与 contract 完成之后。 - -### 12.1 语义查询层 - -可提供: - -- direct/effective/source-aware property query; -- overlay-aware 可选模式; -- 区域一致性判断,例如“整个区域是否同值”; -- run-aware layer snapshot; -- 属性来源解释器:某个最终 face 来自哪个层/category/overlay。 - -这会比再增加几个重载 setter 更能支持调试和复杂 UI。 - -### 12.2 compositional layer - -在现有 exclusive stack 之外,可增加按属性键独立合成的模式: - -```text -top layer: face -middle layer: help-echo -bottom layer: keymap -``` - -最终三个键都可生效,同键才按优先级覆盖。它适合: - -- 语法 + 诊断 + 交互提示并存; -- hover/click/face 分属不同业务来源; -- 响应式状态只更新自己拥有的键。 - -必须通过显式 layer mode 启用,以免改变现有语义。 - -### 12.3 transaction 与冲突检测 - -对复杂 UI,允许在一次事务中: - -- 读取当前 stack version; -- 修改多个层; -- 一次重建; -- 收集 changed ranges; -- 失败时不留下半更新状态。 - -冲突检测可发现原生外部修改,避免下次 stack operation 静默覆盖。 - -### 12.4 增量索引和诊断 - -暴露只读诊断: - -- 某层登记了哪些 buffer; -- 某 buffer 有哪些 managed layer; -- 某层依赖哪些变量; -- 最近一次更新访问/写入多少 run; -- 未追踪但扫描发现了哪些层; -- 匿名层为何仍存活。 - -这能把响应式问题从“猜”变为可观察。 - -### 12.5 theme-aware 生命周期 - -调色板当前在解析颜色时读取 frame 的主题类型 -([`tp-parse-color`](../tp-palette.el#L256-L302)),但静态已应用结果不会自然随 -主题变化。 -可以引入: - -- theme generation/version; -- enable/disable-theme hook 后批量失效; -- 只重渲染真正依赖 palette 的层; -- theme change 的视觉回归。 - -### 12.6 与 font-lock/jit-lock 的协作 - -对于大缓冲区,主动给整段文本写 face 不一定是最佳方案。可探索: - -- 层定义生成 font-lock rule; -- jit-lock 按可见范围物化; -- tp 管理来源身份和规则,Emacs 管理惰性字体化; -- 明确 `face` 与 `font-lock-face` 的优先级和清理策略。 - -这适合语法、诊断等长文档场景,但需要单独的小型验证,不应直接扩展全库。 - -### 12.7 可选核心加载 - -`require 'tp` 当前加载 palette/builtins。未来若安装时间或最小依赖成为真实问题, -可以提供: - -- 核心 façade; -- managed layers/reactive; -- palette/builtins; - -三个明确 feature 入口。只有在有启动性能或依赖数据支持时再做,不为拆分而拆分。 - ---- - -## 13. 推荐的产品定位与文案 - -### 当前版本推荐 - -> tp 为 GNU Emacs 字符串和缓冲区提供高层文本属性操作 API,并在原生属性之上 -> 增加命名层栈、响应式属性、批量匹配和调色板能力。它覆盖常见属性工作流,但 -> category/default/alias、stickiness、undo、yank、special properties 和 -> overlays 的底层行为仍由 Emacs 执行,其中部分尚未成为 tp 的显式 API 契约。 - -### 达到阶段 3 后可使用 - -> tp 是 GNU Emacs 文本属性操作的完整高层 façade:常见原生读取、写入、搜索、 -> 复制和编辑语义均有明确映射,同时可选择受管理层栈和响应式扩展。 - -### 不建议使用 - -> tp 完全替代 Emacs 的所有文本属性和字符属性系统。 - -除非 overlay-aware、category/default/alias、stickiness、undo/yank 等都有明确 -覆盖矩阵和等价性测试,否则这句话无法被验证。 - ---- - -## 14. “可以宣称完整替代”的验收清单 - -只有同时满足以下条件,才建议宣称“完整文本属性操作替代层”: - -### 核心契约 - -- [x] 字符串/缓冲区的修改性在公开入口上可预测; -- [x] 默认坐标是对象原生坐标(`tp-intervals` / `tp-intervals-map` 的 relative 默认为公开兼容例外); -- [x] 所有 text-only 查询区分缺失与显式 nil; -- [x] wildcard 与 VALUE=nil 不复用同一个参数状态; -- [x] 内部查询结果结构不随对象类型改变;部分 public returns 保持历史兼容; -- [x] Stage 1 错误传播与 NOERROR 范围有规范; -- [x] read-only/modified/undo 的三种 mutation policy 组合有测试保证。 - -### 原生覆盖 - -- [x] put/add/set/remove/remove-list/clear 有明确映射; -- [x] text-properties-at/get-text-property 有明确映射; -- [x] property change/search 家族有明确映射; -- [x] category/default/alias 有 direct/effective 区分; -- [x] get-char-property / get-char-property-and-overlay 查询有明确映射; -- [x] stickiness 与插入继承有文档和测试; -- [x] copy/substring/insert/kill/yank 有文档和测试; -- [x] special properties 明确委托给 Emacs,不被 tp 破坏; -- [x] narrowing/indirect buffer 行为通过测试。 - -### managed layers - -- [x] 直接模板展开和 managed layer 实例名称不同、行为不同; -- [x] 层重定义能删除旧贡献; -- [x] hidden/buried layer 始终保存最新 entry; -- [x] 外部直接属性修改不会被静默丢失; -- [x] 插入 propertized string 后的 attach/track 行为明确; -- [x] 参数化 mounted layer args/version refresh 通过测试; -- [x] transaction success/failure rollback 与 diagnostics 通过测试; -- [x] theme generation diagnostics 通过测试。 - -### 工程证据 - -- [x] 原生等价性测试覆盖选定 Emacs 版本; -- [x] 性能基准覆盖大文本、碎片 interval、深层栈和 fan-out(结果为 advisory baseline,不是发布阈值); -- [x] 中英文 README、docstring、doctest 来自同一语义规范; -- [ ] CI 全绿且 warning 为零。 - ---- - -## 15. 最终优先级 - -### 立即处理 - -1. TP-A01:`tp-text` 初次多 interval 属性扩散; -2. TP-A10:阻止 managed hidden range 静默丢弃外部直接属性; -3. TP-A02:静态层重定义与层刷新 old/new 所有权; -4. TP-A03/A04/A05:presence、nil、wildcard、删除状态统一; -5. TP-A06:收窄 `NOERROR`; -6. TP-A11:收敛业务计算与 observer 错误边界; -7. 修复已确认 docstring/实现矛盾。 - -### 下一版本的设计重点 - -1. CI/WERROR/full-suite release evidence 汇总; -2. 可选 compositional layer 的小型验证; -3. theme refresh 的真实依赖定向优化; -4. 将 benchmark advisory baseline 转为 CI 阈值; -5. overlay lifecycle 是否继续排除的长期产品边界复审。 - -### 在语义收敛后再扩展 - -1. compositional layers; -2. transaction/冲突检测; -3. overlay-aware/source-aware 查询; -4. theme-aware 刷新; -5. font-lock/jit-lock 集成; -6. 性能与诊断工具; -7. 可选核心加载。 - ---- - -## 16. 验证记录 - -本次审计在 Emacs 30.2 上完成以下验证: - -```text -compile-all(WERROR=t) 通过 -ERT 604/604 通过 -doctest 92/92 通过 -benchmark 通过,fixed seeds=1/7/42/747555,generated seed=8675309 -shuffled ERT 604/604 通过,seed=747555 -``` - -测试使用仓库 Makefile 和单独加载的 dash 2.20.0。所有动态问题复现都在同一代码 -快照 `a65d799` 上执行。 - -本报告的结论边界是: - -- “已有测试全绿”是已确认事实; -- TP-A01~A06、TP-A10 和 TP-A11 已有回归测试覆盖; -- 性能部分记录 advisory baseline,没有把单机耗时声明为发布阈值; -- overlay 是否进入产品范围是定位决策,但 overlay-aware 查询是任何“字符属性 - 完整替代”声明不可回避的边界。 - -## 17. 官方参考 - -- [GNU Emacs Lisp Reference Manual: Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Text-Properties.html) -- [Examining Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Examining-Properties.html) -- [Changing Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Changing-Properties.html) -- [Text Property Search Functions](https://www.gnu.org/software/emacs/manual/html_node/elisp/Property-Search.html) -- [Why Text Properties are not Intervals](https://www.gnu.org/software/emacs/manual/html_node/elisp/Not-Intervals.html) -- [Stickiness of Text Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Sticky-Properties.html) -- [Properties with Special Meanings](https://www.gnu.org/software/emacs/manual/html_node/elisp/Special-Properties.html) -- [Fields](https://www.gnu.org/software/emacs/manual/html_node/elisp/Fields.html) -- [Yanking](https://www.gnu.org/software/emacs/manual/html_node/elisp/Yanking.html) -- [Insertion](https://www.gnu.org/software/emacs/manual/html_node/elisp/Insertion.html) -- [Narrowing](https://www.gnu.org/software/emacs/manual/html_node/elisp/Narrowing.html) -- [Indirect Buffers](https://www.gnu.org/software/emacs/manual/html_node/elisp/Indirect-Buffers.html) -- [Overlay Properties](https://www.gnu.org/software/emacs/manual/html_node/elisp/Overlay-Properties.html) - -## 18. 最小动态复现 - -以下表达式均在仓库根目录、Emacs 30.2、dash 2.20.0、快照 `a65d799` 上复核。 -通用命令为: - -```sh -EMACS=/Applications/Emacs.app/Contents/MacOS/Emacs -DASH=/path/to/dash-2.20.0 -"$EMACS" -Q --batch -L . -L "$DASH" -l tp.el --eval '' -``` - -将下面每个完整表达式作为 `--eval` 的参数即可。使用其他受支持 Emacs 版本时, -应同时记录版本和输出。 - -### 18.1 TP-A01:初次 `tp-text` 多 interval 污染 - -```elisp -(let* ((payload - (concat - (propertize "AB" (quote face) (quote bold)) - (propertize "CD" (quote face) (quote italic)))) - (result (tp-set "xxxx" (quote tp-text) payload))) - (princ - (format "string: p0=%S p2=%S\n" - (get-text-property 0 (quote face) result) - (get-text-property 2 (quote face) result))) - (with-temp-buffer - (insert "xxxx") - (tp-set 1 5 (list (quote tp-text) payload)) - (princ - (format "buffer: p1=%S p3=%S\n" - (get-text-property 1 (quote face)) - (get-text-property 3 (quote face)))))) -``` - -实际: - -```text -string: p0=bold p2=(bold italic) -buffer: p1=bold p3=bold -``` - -预期后半段只为 `italic`。 - -### 18.2 TP-A02-A:静态层重定义不刷新 - -```elisp -(progn - (tp-layer-reset) - (define-tp audit-static () - (quote (face bold help-echo "old"))) - (with-temp-buffer - (insert "abc") - (tp-push-layer 1 4 (quote audit-static)) - (define-tp audit-static () - (quote (face italic help-echo "new"))) - (princ - (format "face=%S help=%S stack=%S\n" - (get-text-property 1 (quote face)) - (get-text-property 1 (quote help-echo)) - (tp-layer-stack-at 1))))) -``` - -实际: - -```text -face=bold help="old" -stack=((audit-static face bold help-echo "old")) -``` - -### 18.3 TP-A02-B:已触发 refresh 仍残留旧键 - -```elisp -(progn - (tp-layer-reset) - (defvar audit-fg "red") - (defvar audit-bg "blue") - (define-tp audit-reactive () - :props (quote - (face (:foreground $audit-fg) - help-echo "old"))) - (with-temp-buffer - (insert "abc") - (tp-push-layer 1 4 (quote audit-reactive)) - (define-tp audit-reactive () - :props (quote (face (:background $audit-bg)))) - (princ - (format "face=%S help=%S stack=%S\n" - (get-text-property 1 (quote face)) - (get-text-property 1 (quote help-echo)) - (tp-layer-stack-at 1))))) -``` - -实际: - -```text -face=(:background "blue") help="old" -stack=((audit-reactive face (:background "blue") help-echo "old")) -``` - -预期 stack 和直接属性中都不再存在 `help-echo`。 - -### 18.4 TP-A03:显式 nil 被内嵌值覆盖 - -```elisp -(let* ((payload - (propertize "X" (quote custom) (quote embedded))) - (result - (tp-set "x" - (quote tp-text) payload - (quote custom) nil))) - (princ - (format "member=%S props=%S\n" - (tp-member 0 (quote custom) result) - (text-properties-at 0 result)))) -``` - -实际: - -```text -member=(custom embedded) -props=(custom embedded tp-text #("X" 0 1 (custom embedded))) -``` - -预期 `member=(custom nil)`。 - -### 18.5 TP-A04:nil 搜索在字符串与缓冲区中不同 - -```elisp -(let ((s (copy-sequence "abc"))) - (put-text-property 0 1 (quote p) (quote x) s) - (put-text-property 2 3 (quote p) nil s) - (princ - (format "string=%S\n" - (tp-forward (quote p) nil s 1))) - (with-temp-buffer - (insert "abc") - (put-text-property 1 2 (quote p) (quote x)) - (put-text-property 3 4 (quote p) nil) - (goto-char 1) - (let ((match - (tp-forward (quote p) nil (current-buffer) 1))) - (princ - (format - "buffer=%S\n" - (and match - (list (prop-match-beginning match) - (prop-match-end match) - (prop-match-value match)))))))) -``` - -实际: - -```text -string=((0 1 x)) -buffer=(2 4 nil) -``` - -字符串路径把 nil 当作 wildcard,缓冲区路径按 nil 值搜索,并把“缺失”和 -“显式 nil”的相邻范围合并为一个有效值为 nil 的 match。 - -### 18.6 TP-A05:子属性删除后的 presence 不同 - -```elisp -(let* ((s - (propertize "x" - (quote face) - (quote (:underline t)))) - (result - (tp-remove s (quote face) :underline))) - (princ - (format "string=%S props=%S\n" - (tp-member 0 (quote face) result) - (text-properties-at 0 result))) - (with-temp-buffer - (insert - (propertize "x" - (quote face) - (quote (:underline t)))) - (tp-remove 1 2 (quote (face :underline))) - (princ - (format "buffer=%S props=%S\n" - (tp-member 1 (quote face)) - (text-properties-at 1))))) -``` - -实际: - -```text -string=(face nil) props=(face nil) -buffer=nil props=nil -``` - -### 18.7 TP-A06:`NOERROR` 吞掉层 body 错误 - -```elisp -(progn - (define-tp audit-boom (x) - (error "audit boom %S" x)) - (let ((s (copy-sequence "x"))) - (princ - (format "result=%S props=%S\n" - (tp-push-layer - s (quote (audit-boom 1)) t) - (text-properties-at 0 s))))) -``` - -实际: - -```text -result=nil props=nil -``` - -关闭 `NOERROR` 时同一层 body 会正常抛出 `audit boom 1`。 - -### 18.8 TP-A10:外部直接属性被后续栈操作丢弃 - -```elisp -(progn - (tp-layer-reset) - (define-tp audit-hidden () - (quote (face bold))) - (let ((s (copy-sequence "x"))) - (tp-push-layer s (quote audit-hidden)) - (tp-hide-layer s (quote audit-hidden)) - (put-text-property - 0 1 (quote help-echo) "external" s) - (princ - (format "before=%S\n" - (text-properties-at 0 s))) - (tp-show-layer s (quote audit-hidden)) - (princ - (format "after=%S\n" - (text-properties-at 0 s))))) -``` - -实际: - -```text -before=(help-echo "external" - tp-layers ((tp-hidden t face bold tp-name audit-hidden))) -after=(face bold tp-name audit-hidden) -``` - -`help-echo` 在 `tp-show-layer` 后无提示消失。 +- [TP 1.0 公共 API 参考](API-REFERENCE.md) +- [API 语义合同](API-SEMANTICS.md) +- [当前架构](ARCHITECTURE.md) +- [中文 README](../README_CN.md) diff --git a/docs/reactive-optimization-en.md b/docs/reactive-optimization-en.md index 9a20d7e..4000c81 100644 --- a/docs/reactive-optimization-en.md +++ b/docs/reactive-optimization-en.md @@ -1,214 +1,12 @@ -# tp.el Reactive System Optimization Documentation +# Reactive optimization (historical archive) -> **Historical TP 0.3 document; obsolete for TP 1.0.** This document evaluates the removed `$variable`, `tp-text`, inline `tp-name`, `tp-render.el`, and scan-driven batching model and remains only as design history. None of the implementation-status claims, function names, or examples below describe TP 1.0. The current optimization model uses an exact signal/binding dependency graph, transaction batching, and retained-surface diffing; see the [README](../README.md), [current architecture](ARCHITECTURE.md), and [API contract](API-SEMANTICS.md). +This page is a TP 0.3 performance note. Its stack, tp-text, batch-update, +legacy reactive bookkeeping, and scan-renderer discussion is not part of TP 1.0. +It is not a current performance baseline or implementation guide. -This document describes the optimizations and enhancements made to the tp.el reactive system based on practical experience from the [twidget](https://github.com/Kinneyzhang/twidget.git) project. +For the current runtime, read: -## Optimization Suggestions Evaluation - -The following evaluates and documents the implementation status of six optimization suggestions for the tp.el reactive system: - -### 1. Granular Reactive Updates - -**Suggestion**: Support partial updates within a region - only updating the reactive portion while preserving surrounding text properties. - -**Evaluation**: Already implemented. tp.el uses `tp-search-map` over `tp-name`-tagged regions and interval-based update mechanisms to support fine-grained property updates. Updates only affect regions with specific `tp-name` properties, and only the layer's own property keys are replaced — properties contributed by other sources are left untouched. - -### 2. Reactive Symbol Cleanup ✅ Already Implemented - -**Suggestion**: Add a mechanism to unregister reactive symbols when widgets are destroyed. - -**Evaluation**: Already implemented. The `tp--unregister-reactive-deps` function handles cleanup: -- Called automatically when a layer is redefined -- Called automatically when a layer is undefined (`tp-undefine-layer`) -- Cleans up variable watchers, computed properties, and data variables - -**Key functions**: -- `tp--unregister-reactive-deps` -- `tp--unregister-layer-watchers` -- `tp--unregister-layer-computed` -- `tp--unregister-layer-data` - -### 3. Scoped Reactivity ✅ Already Implemented - -**Suggestion**: Add instance/context scoping for reactive variables. - -**Evaluation**: Already implemented. The `where` parameter supports buffer-local updates in: -- `tp--update-layer-regions` -- `tp--update-reactive-text` - -When using `setq-local`, updates only affect the specific buffer. - -### 4. Batched Updates 🆕 New Feature - -**Suggestion**: When multiple reactive values change simultaneously, batch updates to avoid redundant buffer modifications. - -**Implementation**: Added `tp-with-batch-updates` macro: - -```elisp -;; Using batch updates -(tp-with-batch-updates - (setq my-color "red") - (setq my-size 14) - (setq my-text "Hello")) -;; All updates applied to buffer once at the end -``` - -**Key functions and variables**: -- `tp-with-batch-updates` - Batch update macro -- `tp--batch-update-active` - Flag indicating batch mode -- `tp--batch-update-pending` - List of pending updates -- `tp--flush-batch-updates` - Apply all pending updates - -### 5. Value Transformation 🆕 New Feature - -**Suggestion**: Allow registering transformation functions that run when tp-text updates. - -**Implementation**: Added `:transform` option: - -```elisp -;; Define a layer with transformation -(define-tp currency-display () - :props '(face bold tp-text $amount) - :data '((amount . "100")) - :transform (lambda (text) - (format "$%s.00" text))) - -;; After application, 100 displays as $100.00 -``` - -**Key functions and variables**: -- `tp-layer-transforms` - Stores layer transform functions -- Transforms applied in `tp--handle-tp-text-property` and `tp--update-reactive-text` - -### 6. Debug Mode 🆕 New Feature - -**Suggestion**: Add a debug mode to trace reactive updates. - -**Implementation**: Added debug functionality: - -```elisp -;; Enable debug mode -(setq tp-debug-mode t) - -;; Also show debug info in minibuffer -(setq tp-debug-echo t) - -;; View debug log -(tp-debug-show) - -;; Clear debug log -(tp-debug-clear) -``` - -**Key functions and variables**: -- `tp-debug-mode` - Enable/disable debug mode -- `tp-debug-echo` - Whether to echo debug info to minibuffer -- `tp-debug-log` - Log debug information -- `tp-debug-show` - Show debug buffer -- `tp-debug-clear` - Clear debug log - -Debug log includes: -- Variable change notifications (old → new value) -- Layer update tracking -- Batch update start/end -- Transform application info - -## New Features in Detail - -### Batch Updates (tp-with-batch-updates) - -When modifying multiple reactive variables simultaneously, use batch updates to avoid multiple buffer updates: - -```elisp -(define-tp themed-text () - :props '(face (:foreground $fg-color :background $bg-color)) - :data '((fg-color . "white") (bg-color . "black"))) - -(with-temp-buffer - (insert "Hello World") - (tp-set 1 12 'themed-text) - - ;; Without batching: each setq triggers a buffer update - (setq fg-color "yellow") ; First update - (setq bg-color "navy") ; Second update - - ;; With batching: all changes applied once at the end - (tp-with-batch-updates - (setq fg-color "red") - (setq bg-color "blue"))) ; Only one update -``` - -### Value Transformation (:transform) - -Transform functions allow processing tp-text values before display: - -```elisp -;; Number formatting -(define-tp price-display () - :props '(tp-text $price) - :data '((price . "99.9")) - :transform (lambda (text) - (format "$%.2f" (string-to-number text)))) - -;; Date formatting -(define-tp date-display () - :props '(tp-text $timestamp) - :data '((timestamp . "1703865600")) - :transform (lambda (text) - (format-time-string "%Y-%m-%d" - (seconds-to-time (string-to-number text))))) - -;; Uppercase conversion -(define-tp uppercase-text () - :props '(tp-text $content) - :data '((content . "hello")) - :transform #'upcase) -``` - -### Debug Mode - -Debug mode helps developers understand the reactive update flow: - -```elisp -;; Enable full debugging -(setq tp-debug-mode t) -(setq tp-debug-echo t) - -;; Define and use a reactive layer -(define-tp test-layer () - :props '(face (:foreground $my-color)) - :data '((my-color . "red"))) - -(with-temp-buffer - (insert "Test") - (tp-set 1 5 'test-layer) - (setq my-color "blue")) - -;; Example debug output: -;; [12:34:56.789] Variable my-color changed: "red" -> "blue" (where: global) -;; [12:34:56.790] Updating layer test-layer (tp-text affected: no) -``` - -## Architecture Notes - -These optimizations follow tp.el's layered architecture principles: - -1. **Debug Mode** - Basic utility layer functionality (`tp-core.el`) -2. **Batch Updates** - Implemented in the reactive system layer (`tp-reactive.el`) -3. **Value Transformation** - Implemented in layer definition and reactive text handling (`tp-layer.el` / `tp-render.el`) - -All new features integrate seamlessly with the existing reactive system without breaking existing APIs. - -## Function Reference - -| Function/Variable | Description | -|------------------|-------------| -| `tp-debug-mode` | Enable debug mode | -| `tp-debug-echo` | Enable minibuffer debug output | -| `tp-debug-log` | Log debug information | -| `tp-debug-show` | Show debug buffer | -| `tp-debug-clear` | Clear debug log | -| `tp-with-batch-updates` | Batch update macro | -| `tp-layer-transforms` | Layer transform function storage | -| `:transform` | Transform option in layer definition | +- [TP 1.0 public API reference](API-REFERENCE.md) +- [Current architecture](ARCHITECTURE.md) +- [Current benchmark guide](BENCHMARKS.md) +- [README](../README.md) diff --git a/docs/reactive-optimization.md b/docs/reactive-optimization.md index 6be333c..46da8e5 100644 --- a/docs/reactive-optimization.md +++ b/docs/reactive-optimization.md @@ -1,214 +1,12 @@ -# tp.el 响应式系统优化文档 +# Reactive optimization(历史归档) -> **历史 TP 0.3 文档,TP 1.0 已废弃。** 本文评估的是已经删除的 `$variable`、`tp-text`、inline `tp-name`、`tp-render.el` 与扫描式批处理模型,仅作为设计历史保留;下文的实现状态、函数名和示例均不适用于 TP 1.0。当前优化模型建立在精确 signal/binding 依赖图、事务批处理和 retained surface diff 上,详见 [README](../README_CN.md)、[当前架构](ARCHITECTURE.md) 和 [API 合同](API-SEMANTICS.md)。 +本文是 TP 0.3 的性能分析记录,所讨论的 stack、tp-text、batch update、 +旧 reactive bookkeeping 和扫描式 renderer 已不属于 TP 1.0。它不提供当前 +性能基线,也不应作为实现指导。 -本文档基于 [twidget](https://github.com/Kinneyzhang/twidget.git) 项目的实践经验,对 tp.el 的响应式系统进行了优化和增强。 +当前行为和 API 见: -## 优化建议评估 - -以下是针对 tp.el 响应式系统的六项优化建议的评估和实现情况: - -### 1. 细粒度响应式更新(Granular Reactive Updates) - -**建议**:支持区域内的部分更新,只更新响应式部分,保留周围文本属性。 - -**评估**:已经实现。tp.el 通过在带 `tp-name` 标记的区域上使用 `tp-search-map`,以及基于区间的更新机制,已经支持细粒度的属性更新。更新只影响具有特定 `tp-name` 的区域,并且只替换该层自身的属性键——由其他来源贡献的属性保持不变。 - -### 2. 响应式符号清理(Reactive Symbol Cleanup)✅ 已实现 - -**建议**:当 widget 销毁时,添加注销响应式符号的机制。 - -**评估**:已经实现。`tp--unregister-reactive-deps` 函数负责清理: -- 当层被重新定义时自动调用 -- 当层被取消定义(`tp-undefine-layer`)时自动调用 -- 清理变量监听器、计算属性和数据变量 - -**关键函数**: -- `tp--unregister-reactive-deps` -- `tp--unregister-layer-watchers` -- `tp--unregister-layer-computed` -- `tp--unregister-layer-data` - -### 3. 作用域响应式(Scoped Reactivity)✅ 已实现 - -**建议**:为响应式变量添加实例/上下文作用域。 - -**评估**:已经实现。`where` 参数在以下函数中支持缓冲区局部更新: -- `tp--update-layer-regions` -- `tp--update-reactive-text` - -当使用 `setq-local` 时,更新只影响特定缓冲区。 - -### 4. 批量更新(Batched Updates)🆕 新增 - -**建议**:当多个响应式值同时变化时,批量处理更新以避免冗余的缓冲区修改。 - -**实现**:新增 `tp-with-batch-updates` 宏: - -```elisp -;; 使用批量更新 -(tp-with-batch-updates - (setq my-color "red") - (setq my-size 14) - (setq my-text "Hello")) -;; 所有更新在批量结束后一次性应用到缓冲区 -``` - -**关键函数和变量**: -- `tp-with-batch-updates` - 批量更新宏 -- `tp--batch-update-active` - 标记是否在批量更新中 -- `tp--batch-update-pending` - 待处理的更新列表 -- `tp--flush-batch-updates` - 应用所有待处理更新 - -### 5. 值转换(Value Transformation)🆕 新增 - -**建议**:允许注册转换函数,在 tp-text 更新时运行。 - -**实现**:新增 `:transform` 选项: - -```elisp -;; 定义带转换的层 -(define-tp currency-display () - :props '(face bold tp-text $amount) - :data '((amount . "100")) - :transform (lambda (text) - (format "$%s.00" text))) - -;; 使用后,100 会显示为 $100.00 -``` - -**关键函数和变量**: -- `tp-layer-transforms` - 存储层转换函数 -- 转换在 `tp--handle-tp-text-property` 和 `tp--update-reactive-text` 中应用 - -### 6. 调试模式(Debug Mode)🆕 新增 - -**建议**:添加调试模式以追踪响应式更新。 - -**实现**:新增调试功能: - -```elisp -;; 启用调试模式 -(setq tp-debug-mode t) - -;; 同时在 minibuffer 显示调试信息 -(setq tp-debug-echo t) - -;; 查看调试日志 -(tp-debug-show) - -;; 清除调试日志 -(tp-debug-clear) -``` - -**关键函数和变量**: -- `tp-debug-mode` - 启用/禁用调试模式 -- `tp-debug-echo` - 是否在 minibuffer 显示调试信息 -- `tp-debug-log` - 记录调试信息 -- `tp-debug-show` - 显示调试缓冲区 -- `tp-debug-clear` - 清除调试日志 - -调试日志包含: -- 变量变化通知(旧值 → 新值) -- 层更新追踪 -- 批量更新开始/结束 -- 转换应用信息 - -## 新增功能详解 - -### 批量更新 (tp-with-batch-updates) - -当需要同时修改多个响应式变量时,使用批量更新可以避免多次缓冲区更新: - -```elisp -(define-tp themed-text () - :props '(face (:foreground $fg-color :background $bg-color)) - :data '((fg-color . "white") (bg-color . "black"))) - -(with-temp-buffer - (insert "Hello World") - (tp-set 1 12 'themed-text) - - ;; 不使用批量更新:每个 setq 都会触发一次缓冲区更新 - (setq fg-color "yellow") ; 第一次更新 - (setq bg-color "navy") ; 第二次更新 - - ;; 使用批量更新:所有变化在结束时一次性应用 - (tp-with-batch-updates - (setq fg-color "red") - (setq bg-color "blue"))) ; 只更新一次 -``` - -### 值转换 (:transform) - -转换函数允许在显示前处理 tp-text 的值: - -```elisp -;; 数字格式化 -(define-tp price-display () - :props '(tp-text $price) - :data '((price . "99.9")) - :transform (lambda (text) - (format "$%.2f" (string-to-number text)))) - -;; 日期格式化 -(define-tp date-display () - :props '(tp-text $timestamp) - :data '((timestamp . "1703865600")) - :transform (lambda (text) - (format-time-string "%Y-%m-%d" - (seconds-to-time (string-to-number text))))) - -;; 大写转换 -(define-tp uppercase-text () - :props '(tp-text $content) - :data '((content . "hello")) - :transform #'upcase) -``` - -### 调试模式 - -调试模式帮助开发者理解响应式更新流程: - -```elisp -;; 启用完整调试 -(setq tp-debug-mode t) -(setq tp-debug-echo t) - -;; 定义和使用响应式层 -(define-tp test-layer () - :props '(face (:foreground $my-color)) - :data '((my-color . "red"))) - -(with-temp-buffer - (insert "Test") - (tp-set 1 5 'test-layer) - (setq my-color "blue")) - -;; 调试输出示例: -;; [12:34:56.789] Variable my-color changed: "red" -> "blue" (where: global) -;; [12:34:56.790] Updating layer test-layer (tp-text affected: no) -``` - -## 架构说明 - -这些优化遵循 tp.el 的分层架构原则: - -1. **调试模式** - 作为基础工具层功能(`tp-core.el`) -2. **批量更新** - 在响应式系统层实现(`tp-reactive.el`) -3. **值转换** - 在层定义和响应式文本处理中实现(`tp-layer.el` / `tp-render.el`) - -所有新功能都与现有的响应式系统无缝集成,不破坏现有 API。 - -## 相关函数一览 - -| 函数/变量 | 描述 | -|----------|------| -| `tp-debug-mode` | 启用调试模式 | -| `tp-debug-echo` | 启用 minibuffer 调试输出 | -| `tp-debug-log` | 记录调试信息 | -| `tp-debug-show` | 显示调试缓冲区 | -| `tp-debug-clear` | 清除调试日志 | -| `tp-with-batch-updates` | 批量更新宏 | -| `tp-layer-transforms` | 层转换函数存储 | -| `:transform` | 层定义中的转换选项 | +- [TP 1.0 公共 API 参考](API-REFERENCE.md) +- [当前架构](ARCHITECTURE.md) +- [当前 benchmark 说明](BENCHMARKS.md) +- [README](../README_CN.md) diff --git a/docs/reactive-text-properties-en.md b/docs/reactive-text-properties-en.md index d9b0469..562fb56 100644 --- a/docs/reactive-text-properties-en.md +++ b/docs/reactive-text-properties-en.md @@ -1,510 +1,17 @@ -# tp.el Complete Guide to Reactive Text Properties +# Reactive text properties (historical archive) -> **Historical TP 0.3 document; obsolete for TP 1.0.** This document records the removed `$variable`, `tp-text`, inline `tp-name`, and scan-driven reactive-update model and remains only as migration and design history. None of the APIs, examples, or “current behavior” claims below describe TP 1.0. The current reactive model uses signals, bindings, `tp-computed`, `tp-watch`, and retained surfaces; see the [README](../README.md), [current architecture](ARCHITECTURE.md), and [API contract](API-SEMANTICS.md). +This page records TP 0.3-era design and implementation work. It is not a TP 1.0 +usage guide. The old tp-text, tp-render, tp-stack, $variable, inline metadata, +and scan-driven refresh behavior described by the historical material was +removed from the current runtime. Do not copy its APIs or examples. -> Bringing modern frontend framework reactive programming paradigms to the Emacs text properties world +Read the current documentation instead: -## Introduction +- [TP 1.0 public API reference](API-REFERENCE.md) +- [API semantics](API-SEMANTICS.md) +- [Current architecture](ARCHITECTURE.md) +- [README](../README.md) -In traditional Emacs development, managing text properties has always been a tedious task. Whenever you want to change a property value, you need to manually find all related text regions and update them one by one. This approach is not only error-prone but also difficult to maintain. - -**Reactive Text Properties** is one of the most innovative features in the tp.el library. It borrows the reactive programming concepts from modern frontend frameworks like Vue.js and React, allowing Emacs text properties to **automatically respond to variable changes**. - -Imagine: you define the relationship between a variable and a property once, and from then on, whenever you change the variable's value, all text regions using that variable will **automatically update**. This is the magic of reactive text properties! - -## From Traditional to Reactive - -### Pain Points of the Traditional Approach - -Let's first look at how the traditional approach handles dynamic text properties: - -```lisp -;; Traditional approach: define a color variable -(defvar my-color "red") - -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - (tp-set 1 12 `(face (:foreground ,my-color))) - - ;; Here comes the problem: when you want to change the color... - (setq my-color "blue") - ;; The text doesn't update automatically! You must manually reapply: - (tp-set 1 12 `(face (:foreground ,my-color)))) -``` - -The problems with this approach are obvious: -1. **Manual tracking**: You need to remember which text regions use which variables -2. **Easy to miss**: In complex applications, it's easy to forget to update some regions -3. **Code redundancy**: Update logic is scattered throughout the code - -### The Elegance of Reactive Approach - -Now let's see how the reactive approach solves these problems: - -```lisp -;; Reactive approach: define a color variable -(defvar my-color "red") - -;; Define a reactive layer using $my-color to reference the variable -(define-tp my-highlight () - '(face (:foreground $my-color))) - -;; Apply to text -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - (tp-set 1 12 'my-highlight) - - ;; Now, just change the variable! - (setq my-color "blue") - ;; Magic happens: the text automatically turns blue! - ) -``` - -Isn't that amazing? Let's dive deep into how this powerful feature works. - -## Core Concepts - -### Reactive Variables - -In tp.el, any symbol starting with `$` is treated as a **reactive variable**. For example: -- `$my-color` → references variable `my-color` -- `$font-size` → references variable `font-size` -- `$theme-background` → references variable `theme-background` - -When you use these `$`-prefixed symbols in property definitions, tp.el will: -1. Automatically resolve the variable's current value -2. Register a watcher to monitor variable changes -3. When the variable changes, automatically update all related text regions - -## Basic Usage - -### Your First Reactive Layer - -Let's start with a simple example: - -```lisp -;; Define a global variable -(defvar highlight-bg "yellow") - -;; Define a reactive layer -(define-tp simple-highlight () - '(face (:background $highlight-bg))) - -;; Create a test buffer and apply the layer -(tp-pop-to-buffer "*tp-test*" - (insert "This is text that needs highlighting") - (tp-set 1 (point-max) 'simple-highlight) - ;; => "Initial background color: yellow" - ;; Change the variable - (setq highlight-bg "cyan") - ;; => "Updated background color: cyan" - ) -``` - -### Multiple Reactive Variables - -A layer can reference multiple reactive variables: - -```lisp -;; Define multiple variables -(defvar fg-color "white") -(defvar bg-color "darkGreen") -(defvar underline-color "red") - -;; Define a layer using multiple variables -(define-tp multi-var-layer () - '(face ( :foreground $fg-color - :background $bg-color - :underline (:color $underline-color)))) - -;; Test -(tp-pop-to-buffer "*tp-test*" - (insert "Multi-variable reactive example") - (tp-set 1 (point-max) 'multi-var-layer) - - ;; Changing any variable triggers an update - (setq fg-color "yellow") ; Foreground turns yellow - (setq bg-color "navy") ; Background turns navy - (setq underline-color "lime") ; Underline turns lime green - ) -``` - -## Advanced Features: :data, :compute, and :watch - -tp.el's reactive system borrows from Vue's API, providing three powerful keywords: - -### :data - Define Additional Reactive State - -Sometimes you need reactive variables that aren't directly used in `:props`. This is where `:data` comes in. - -Main uses of `:data`: -1. Define auxiliary variables that don't appear directly in properties -2. Provide initial values for variables -3. Work together with `:compute` - -### :compute - Computed Properties - -`:compute` lets you define **derived values**—their values are computed from other variables: - -```lisp -;; Complete computed properties example -(define-tp computed-greeting () - :props '(display $full-greeting face (:foreground $status-color)) - :data '((user-name . "John") - (greeting-prefix . "Hello")) - :compute '((full-greeting (lambda () - (format "%s, %s! Welcome back." - greeting-prefix user-name))) - (status-color (lambda () - (if (string= user-name "Admin") - "red" - "green"))))) - -;; Test -(tp-pop-to-buffer "*tp-test*" - (insert "Test text") - (tp-set 1 (point-max) 'computed-greeting) - ;; Initial state - (message "full-greeting = %s" full-greeting) - ;; => "Hello, John! Welcome back." - (message "status-color = %s" status-color) - ;; => "green" - ;; Change user-name - (setq user-name "Admin") - ;; Computed properties update automatically! - (message "full-greeting = %s" full-greeting) - ;; => "Hello, Admin! Welcome back." - (message "status-color = %s" status-color) - ;; => "red" - ;; Change greeting-prefix - (setq greeting-prefix "Hi") - (message "full-greeting = %s" full-greeting)) -;; => "Hi, Admin! Welcome back." -``` - -### :watch - Watch Variable Changes - -`:watch` lets you execute **side effect** operations when variables change: - -```lisp -;; Layer with watchers -(define-tp watched-layer () - :props '(face (:foreground $status-color)) - :data '((status-color . "green")) - :watch '((status-color - (lambda (new-val old-val layer-name) - (message "[%s] Color changed from %s to %s" - layer-name old-val new-val))))) - -;; Test -(tp-pop-to-buffer "*tp-test*" - (insert "Test text") - (tp-set 1 (point-max) 'watched-layer) - - ;; Change color - triggers watcher - (setq status-color "yellow") - ;; Message: "[watched-layer] Color changed from green to yellow" - - (setq status-color "red")) -;; Message: "[watched-layer] Color changed from yellow to red" -``` - -Typical uses for `:watch`: -- Logging -- Updating external state -- Triggering notifications -- Performing cleanup operations - -## Complete Practical Examples - -### Example 1: Dynamic Color Status Indicator - -This example shows how to create an indicator that automatically changes color based on status: - -```lisp -(tp-layer-reset) - -;; Define status color variables -(defvar status-color "gray") -(defvar status-text "Not Started") - -;; Define status indicator layer -(define-tp status-indicator () - '(face (:background $status-color) display $status-text)) - -;; Define status update function -(defun set-status (status) - "Set status, automatically update color and text" - (pcase status - ('pending (setq status-color "gray" status-text "Pending")) - ('running (setq status-color "blue" status-text "Running")) - ('success (setq status-color "green" status-text "Success")) - ('warning (setq status-color "orange" status-text "Warning")) - ('error (setq status-color "red" status-text "Error")))) - -;; Test the status indicator -(tp-pop-to-buffer "*tp-test*" - (insert "Status") - (tp-set 1 (point-max) 'status-indicator) - - ;; Simulate status changes - (set-status 'pending) - (message "Status: %s, Color: %s" status-text status-color) - ;; => "Status: Pending, Color: gray" - - (set-status 'running) - (message "Status: %s, Color: %s" status-text status-color) - ;; => "Status: Running, Color: blue" - - (set-status 'success) - (message "Status: %s, Color: %s" status-text status-color)) -;; => "Status: Success, Color: green" -``` - -### Example 2: Theme Switching System - -This example shows how to create a switchable theme system: - -```lisp -(tp-layer-reset) - -;; Define theme color variables -(defvar keyword-color nil) -(defvar string-color nil) - -;; Define theme-related reactive layers -(define-tp themed-keyword () - '(face (:foreground $keyword-color :weight bold))) - -(define-tp themed-string () - '(face (:foreground $string-color))) - -;; Define theme switching functions -(defun switch-to-dark-theme () - "Switch to dark theme" - (interactive) - (setq keyword-color "light blue" - string-color "green") - (message "Switched to dark theme")) - -(defun switch-to-light-theme () - "Switch to light theme" - (interactive) - (setq keyword-color "blue" - string-color "dark green") - (message "Switched to light theme")) - -;; Test theme switching -(tp-pop-to-buffer "*tp-test*" - (insert "(defun hello () \"greeting\")") - - ;; Apply different theme layers - (tp-match-set "defun" 'themed-keyword) - (tp-regexp-set "\".+\"" 'themed-string) - - (switch-to-dark-theme) - ;; Initially using dark theme - (message "Keyword color: %s" keyword-color) - (message "String color: %s" string-color) - - ;; Switch to light theme - (switch-to-light-theme) - ;; Text updates automatically! - (message "Keyword color: %s" keyword-color) - (message "String color: %s" string-color)) -``` - -## Anonymous Reactive Layers - -Besides using `define-tp` to define named layers, you can also use reactive variables directly in property lists. tp.el will automatically generate unique names for these anonymous layers: - -```lisp -(tp-layer-reset) - -(defvar inline-color "purple") - -(tp-pop-to-buffer "*tp-test*" - (insert "Anonymous reactive layer example") - - ;; Use $inline-color directly, no need to pre-define a layer - (tp-set 1 (point-max) '(face (:foreground $inline-color))) - - ;; Text is now purple - (message "Color: %s" (plist-get (tp-at 1 'face) :foreground)) - ;; => "purple" - - ;; Change the variable - (setq inline-color "orange") - - ;; Text automatically turns orange - (message "Color: %s" (plist-get (tp-at 1 'face) :foreground))) -;; => "orange" -``` - -Anonymous reactive layers are suitable for simple scenarios where you don't need to reuse the same layer definition in multiple places. - -## Reactive Text (tp-text) - -Besides reactive text **properties**, tp.el also supports reactive **text content** itself. Through the special `tp-text` property, you can make the text content reactive too—when the bound variable changes, the text content automatically updates. - -### Basic Usage - -The `tp-text` property has two ways to use: - -#### 1. Initialize with Current Text - -When `tp-text` is `nil`, it will be automatically set to the current region's text content: - -```lisp -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - ;; When tp-text is nil, auto-initialize to current text "Hello" - (tp-set 1 6 '(face bold tp-text nil)) - ;; Now tp-text value is "Hello" - (message "tp-text = %s" (tp-at 1 'tp-text))) -;; => "Hello" -``` - -#### 2. Replace Text Content - -When `tp-text` is a string, it replaces the text in the region while preserving other text properties: - -```lisp -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - ;; When tp-text is a string, replace the text content - (tp-set 1 6 '(face bold tp-text "Hi")) - ;; Text becomes "Hi World", and "Hi" still has bold style - (message "buffer = %s" (buffer-string))) -;; => "Hi World" -``` - -### Reactive Text Layers - -The real power of `tp-text` comes from combining it with reactive variables: - -```lisp -;; Define a reactive variable -(defvar my-dynamic-text "Loading...") - -;; Define a layer containing tp-text -(define-tp dynamic-content () - :props '(face (:foreground "blue") tp-text $my-dynamic-text)) - -;; Apply to text -(tp-pop-to-buffer "*tp-test*" - (insert "placeholder") - (tp-set 1 12 'dynamic-content) - ;; Text now shows "Loading..." - (message "Initial text: %s" (buffer-string)) - ;; => "Loading..." - - ;; Change the variable - (setq my-dynamic-text "Data loaded successfully!") - ;; Text updates automatically! - (message "After update: %s" (buffer-string))) -;; => "Data loaded successfully!" -``` - -### Using :compute for Dynamic Text - -`tp-text` can be combined with `:compute` to create dynamic text derived from other variables: - -```lisp -(define-tp greeting-layer () - :props '(face (:foreground "green") tp-text $full-greeting) - :data '((user-name . "Guest") - (greeting-prefix . "Welcome")) - :compute '((full-greeting - (lambda () - (format "%s, %s!" greeting-prefix user-name))))) - -;; Apply to text -(tp-pop-to-buffer "*tp-test*" - (insert "placeholder") - (tp-set 1 12 'greeting-layer) - ;; Shows "Welcome, Guest!" - (message "Initial: %s" (buffer-string)) - - ;; Change user name - (setq user-name "John") - ;; Text automatically updates to "Welcome, John!" - (message "After update: %s" (buffer-string))) -``` - -### Anonymous Reactive Text - -You can also use reactive `tp-text` directly in property lists without defining a layer: - -```lisp -(defvar inline-text "Original content") - -(tp-pop-to-buffer "*tp-test*" - (insert "placeholder") - ;; Directly use reactive tp-text - (tp-set 1 12 '(face bold tp-text $inline-text)) - ;; Shows "Original content" - - ;; Change the variable - (setq inline-text "New content") - ;; Text automatically updates to "New content" - ) -``` - -### Important Notes - -1. **tp-text on strings returns a new string**: Emacs strings cannot change length in place, so string-object calls return a new string instead of modifying the original. A sub-region `tp-text` replaces only that region and keeps the rest of the string; the whole-string form returns just the replacement text. -2. **Preserves existing properties**: When using `tp-set` or `tp-add` to set `tp-text`, existing text properties are preserved. -3. **Non-reactive properties don't add tp-name**: If there are no reactive variables (`$` prefix) in the text properties, `tp-name` and other reactive-specific properties won't be added, maintaining native text property behavior. - -## Value Transformation with :transform - -The `:transform` keyword allows you to register a transformation function that processes `tp-text` values before they are displayed. This is useful for formatting numbers, dates, or other values: - -```lisp -;; Number formatting -(define-tp price-display () - :props '(tp-text $price) - :data '((price . "99.9")) - :transform (lambda (text) - (format "$%.2f" (string-to-number text)))) -;; 99.9 displays as $99.90 - -;; Date formatting -(define-tp date-display () - :props '(tp-text $timestamp) - :data '((timestamp . "1703865600")) - :transform (lambda (text) - (format-time-string "%Y-%m-%d" - (seconds-to-time (string-to-number text))))) - -;; Uppercase conversion -(define-tp uppercase-text () - :props '(tp-text $content) - :data '((content . "hello")) - :transform #'upcase) -;; "hello" displays as "HELLO" -``` - -The transform function: -- Receives the raw `tp-text` string value -- Returns the transformed string for display -- Is applied both on initial display and reactive updates -- Must return a string; errors and non-string results propagate rather - than leaving stale output visible - -> 📖 **For more optimization features like batched updates and debug mode, see [Reactive System Optimization](reactive-optimization-en.md)** - -## Summary - -tp.el's reactive text properties feature brings a modern reactive programming experience to Emacs development. By using `$`-prefixed reactive variables, `:data` to define state, `:compute` for derived values, `:watch` to monitor changes, and `:transform` for value formatting, you can build a more dynamic and maintainable text property system. - -Key points: -1. **Reactive Variables**: Use `$` prefix to reference variables -2. **:props**: Define properties containing reactive variables -3. **:data**: Define additional reactive state and initial values -4. **:compute**: Define computed properties derived from other variables -5. **:watch**: Watch variable changes and execute side effects -6. **:transform**: Transform tp-text values before display -7. **Automatic Updates**: Change variable values, all related text updates automatically -8. **Reactive Text (tp-text)**: Make text content itself reactive +In TP 1.0, use direct property APIs/recipes for static declarations, tp-watch +for reactive properties on host-owned text, and a content surface for text +owned by TP. diff --git a/docs/reactive-text-properties.md b/docs/reactive-text-properties.md index db1101a..94f3142 100644 --- a/docs/reactive-text-properties.md +++ b/docs/reactive-text-properties.md @@ -1,509 +1,15 @@ -# tp.el 响应式文本属性完全指南 +# Reactive text properties(历史归档) -> **历史 TP 0.3 文档,TP 1.0 已废弃。** 本文记录已经删除的 `$variable`、`tp-text`、inline `tp-name` 和扫描式响应更新模型,仅作为迁移与设计历史保留;下文的 API、示例和“当前行为”声明均不适用于 TP 1.0。当前响应式模型使用 signal、binding、`tp-computed`、`tp-watch` 与 retained surface,详见 [README](../README_CN.md)、[当前架构](ARCHITECTURE.md) 和 [API 合同](API-SEMANTICS.md)。 +本文是 TP 0.3 时代的设计/实现记录,不是 TP 1.0 用法文档。正文中出现的 +tp-text、tp-render、tp-stack、$variable、inline metadata、扫描式刷新等 +内容已经从当前运行时删除;不要复制其中的 API 或示例。 -> 将现代前端框架的响应式编程范式带入 Emacs 文本属性世界 +当前实现请阅读: -## 引言 +- [TP 1.0 公共 API 参考](API-REFERENCE.md) +- [API 语义合同](API-SEMANTICS.md) +- [当前架构](ARCHITECTURE.md) +- [中文 README](../README_CN.md) -在传统的 Emacs 开发中,文本属性(text properties)的管理一直是一个繁琐的任务。每当你想要改变某个属性值时,你需要手动找到所有相关的文本区域,然后逐一更新它们。这种方式不仅容易出错,而且难以维护。 - -**响应式文本属性**是 tp.el 库中最具创新性的功能之一。它借鉴了 Vue.js、React 等现代前端框架的响应式编程思想,让 Emacs 的文本属性能够**自动响应变量的变化**。 - -想象一下:你只需要定义一次变量与属性的关系,之后无论何时改变变量的值,所有使用该变量的文本区域都会**自动更新**。这就是响应式文本属性的魔力! - -## 从传统方式到响应式方式 - -### 传统方式的痛点 - -让我们先看看传统方式如何处理动态文本属性: - -```lisp -;; 传统方式:定义一个颜色变量 -(defvar my-color "red") - -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - (tp-set 1 12 `(face (:foreground ,my-color))) - - ;; 问题来了:当你想改变颜色时... - (setq my-color "blue") - ;; 文本不会自动更新!你必须手动重新应用: - (tp-set 1 12 `(face (:foreground ,my-color)))) -``` - -这种方式的问题显而易见: -1. **手动追踪**:你需要记住哪些文本区域使用了哪些变量 -2. **容易遗漏**:在复杂应用中很容易忘记更新某些区域 -3. **代码冗余**:更新逻辑散落在代码各处 - -### 响应式方式的优雅 - -现在让我们看看响应式方式如何解决这些问题: - -```lisp -;; 响应式方式:定义一个颜色变量 -(defvar my-color "red") - -;; 定义一个响应式层,使用 $my-color 引用变量 -(define-tp my-highlight () - '(face (:foreground $my-color))) - -;; 应用到文本 -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - (tp-set 1 12 'my-highlight) - - ;; 现在,只需改变变量! - (setq my-color "blue") - ;; 神奇的事情发生了:文本自动变成蓝色! - ) -``` - -是不是很神奇?让我们深入了解这个强大功能的工作原理。 - -## 核心概念 - -### 响应式变量 - -在 tp.el 中,任何以 `$` 符号开头的符号都被视为**响应式变量**。例如: -- `$my-color` → 引用变量 `my-color` -- `$font-size` → 引用变量 `font-size` -- `$theme-background` → 引用变量 `theme-background` - -当你在属性定义中使用这些 `$` 前缀的符号时,tp.el 会: -1. 自动解析变量的当前值 -2. 注册一个监听器,监视变量的变化 -3. 当变量改变时,自动更新所有相关的文本区域 - -## 基础用法 - -### 第一个响应式层 - -让我们从一个简单的例子开始: - -```lisp -;; 定义一个全局变量 -(defvar highlight-bg "yellow") - -;; 定义响应式层 -(define-tp simple-highlight () - '(face (:background $highlight-bg))) - -;; 创建测试缓冲区并应用层 -(tp-pop-to-buffer "*tp-test*" - (insert "这是一段需要高亮的文本") - (tp-set 1 (point-max) 'simple-highlight) - ;; => "初始背景色: yellow" - ;; 改变变量 - (setq highlight-bg "cyan") - ;; => "更新后背景色: cyan" - ) -``` - -### 多个响应式变量 - -一个层可以引用多个响应式变量: - -```lisp -;; 定义多个变量 -(defvar fg-color "white") -(defvar bg-color "darkGreen") -(defvar underline-color "red") - -;; 定义使用多个变量的层 -(define-tp multi-var-layer () - '(face ( :foreground $fg-color - :background $bg-color - :underline (:color $underline-color)))) - -;; 测试 -(tp-pop-to-buffer "*tp-test*" - (insert "多变量响应式示例") - (tp-set 1 (point-max) 'multi-var-layer) - - ;; 改变任何一个变量都会触发更新 - (setq fg-color "yellow") ; 前景色变黄 - (setq bg-color "navy") ; 背景色变海军蓝 - (setq underline-color "lime") ; 下划线变酸橙绿 - ) -``` - -## 进阶功能::data、:compute 和 :watch - -tp.el 的响应式系统借鉴了 Vue 的 API,提供了三个强大的关键字: - -### :data - 定义额外的响应式状态 - -有时候你需要一些响应式变量,但它们不直接用于 `:props` 中。这时可以使用 `:data`。 - -`:data` 的主要用途: -1. 定义不直接出现在属性中的辅助变量 -2. 为变量提供初始值 -3. 与 `:compute` 配合使用 - -### :compute - 计算属性 - -`:compute` 让你可以定义**派生值**——它们的值由其他变量计算得出: - -```lisp -;; 完整的计算属性示例 -(define-tp computed-greeting () - :props '(display $full-greeting face (:foreground $status-color)) - :data '((user-name . "张三") - (greeting-prefix . "你好")) - :compute '((full-greeting (lambda () - (format "%s, %s!欢迎回来。" - greeting-prefix user-name))) - (status-color (lambda () - (if (string= user-name "管理员") - "red" - "green"))))) - -;; 测试 -(tp-pop-to-buffer "*tp-test*" - (insert "测试文本") - (tp-set 1 (point-max) 'computed-greeting) - ;; 初始状态 - (message "full-greeting = %s" full-greeting) - ;; => "你好, 张三!欢迎回来。" - (message "status-color = %s" status-color) - ;; => "green" - ;; 改变 user-name - (setq user-name "管理员") - ;; 计算属性自动更新! - (message "full-greeting = %s" full-greeting) - ;; => "你好, 管理员!欢迎回来。" - (message "status-color = %s" status-color) - ;; => "red" - ;; 改变 greeting-prefix - (setq greeting-prefix "您好") - (message "full-greeting = %s" full-greeting)) -;; => "您好, 管理员!欢迎回来。" -``` - -### :watch - 监听变量变化 - -`:watch` 让你可以在变量改变时执行**副作用**操作: - -```lisp -;; 带监听器的层 -(define-tp watched-layer () - :props '(face (:foreground $status-color)) - :data '((status-color . "green")) - :watch '((status-color - (lambda (new-val old-val layer-name) - (message "【%s】颜色从 %s 变为 %s" - layer-name old-val new-val))))) - -;; 测试 -(tp-pop-to-buffer "*tp-test*" - (insert "测试文本") - (tp-set 1 (point-max) 'watched-layer) - - ;; 改变颜色 - 触发监听器 - (setq status-color "yellow") - ;; 消息: "【watched-layer】颜色从 green 变为 yellow" - - (setq status-color "red")) -;; 消息: "【watched-layer】颜色从 yellow 变为 red" -``` - -`:watch` 的典型用途: -- 记录日志 -- 更新外部状态 -- 触发通知 -- 执行清理操作 - -## 完整实战示例 - -### 示例一:动态颜色状态指示器 - -这个示例展示如何创建一个根据状态自动变色的指示器: - -```lisp -(tp-layer-reset) - -;; 定义状态颜色变量 -(defvar status-color "gray") -(defvar status-text "未开始") - -;; 定义状态指示器层 -(define-tp status-indicator () - '(face (:background $status-color) display $status-text)) - -;; 定义状态更新函数 -(defun set-status (status) - "设置状态,自动更新颜色和文本" - (pcase status - ('pending (setq status-color "gray" status-text "待处理")) - ('running (setq status-color "blue" status-text "运行中")) - ('success (setq status-color "green" status-text "成功")) - ('warning (setq status-color "orange" status-text "警告")) - ('error (setq status-color "red" status-text "错误")))) - -;; 测试状态指示器 -(tp-pop-to-buffer "*tp-test*" - (insert "状态") - (tp-set 1 (point-max) 'status-indicator) - - ;; 模拟状态变化 - (set-status 'pending) - (message "状态: %s, 颜色: %s" status-text status-color) - ;; => "状态: 待处理, 颜色: gray" - - (set-status 'running) - (message "状态: %s, 颜色: %s" status-text status-color) - ;; => "状态: 运行中, 颜色: blue" - - (set-status 'success) - (message "状态: %s, 颜色: %s" status-text status-color)) -;; => "状态: 成功, 颜色: green" -``` - -### 示例二:主题切换系统 - -这个示例展示如何创建一个可切换的主题系统: - -```lisp -(tp-layer-reset) - -;; 定义主题颜色变量 -(defvar keyword-color nil) -(defvar string-color nil) - -;; 定义主题相关的响应式层 -(define-tp themed-keyword () - '(face (:foreground $keyword-color :weight bold))) - -(define-tp themed-string () - '(face (:foreground $string-color))) - -;; 定义主题切换函数 -(defun switch-to-dark-theme () - "切换到深色主题" - (interactive) - (setq keyword-color "light blue" - string-color "green") - (message "已切换到深色主题")) - -(defun switch-to-light-theme () - "切换到浅色主题" - (interactive) - (setq keyword-color "blue" - string-color "dark green") - (message "已切换到浅色主题")) - -;; 测试主题切换 -(tp-pop-to-buffer "*tp-test*" - (insert "(defun hello () \"greeting\")") - - ;; 应用不同的主题层 - (tp-match-set "defun" 'themed-keyword) - (tp-regexp-set "\".+\"" 'themed-string) - - (switch-to-dark-theme) - ;; 初始是深色主题 - (message "关键字颜色: %s" keyword-color) - (message "字符串颜色: %s" string-color) - - ;; 切换到浅色主题 - (switch-to-light-theme) - ;; 文本自动更新! - (message "关键字颜色: %s" keyword-color) - (message "字符串颜色: %s" string-color)) -``` - -## 匿名响应式层 - -除了使用 `define-tp` 定义命名层,你还可以直接在属性列表中使用响应式变量。tp.el 会自动为这些匿名层生成唯一的名称: - -```lisp -(tp-layer-reset) - -(defvar inline-color "purple") - -(tp-pop-to-buffer "*tp-test*" - (insert "匿名响应式层示例") - - ;; 直接使用 $inline-color,无需预先定义层 - (tp-set 1 (point-max) '(face (:foreground $inline-color))) - - ;; 文本现在是紫色的 - (message "颜色: %s" (plist-get (tp-at 1 'face) :foreground)) - ;; => "purple" - - ;; 改变变量 - (setq inline-color "orange") - - ;; 文本自动变成橙色 - (message "颜色: %s" (plist-get (tp-at 1 'face) :foreground))) -;; => "orange" -``` - -匿名响应式层适用于简单的场景,当你不需要在多个地方复用同一个层定义时。 - -## 响应式文本 (tp-text) - -除了响应式文本**属性**,tp.el 还支持响应式**文本内容**本身。通过特殊的 `tp-text` 属性,你可以让文本内容也变成响应式的——当绑定的变量改变时,文本内容会自动更新。 - -### 基本用法 - -`tp-text` 属性有两种使用方式: - -#### 1. 初始化当前文本 - -当 `tp-text` 的值为 `nil` 时,它会被自动设置为当前区域的文本内容: - -```lisp -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - ;; tp-text 为 nil 时,自动初始化为当前文本 "Hello" - (tp-set 1 6 '(face bold tp-text nil)) - ;; 现在 tp-text 的值是 "Hello" - (message "tp-text = %s" (tp-at 1 'tp-text))) -;; => "Hello" -``` - -#### 2. 替换文本内容 - -当 `tp-text` 的值为字符串时,它会替换区域内的文本,同时保留其他文本属性: - -```lisp -(tp-pop-to-buffer "*tp-test*" - (insert "Hello World") - ;; tp-text 为字符串时,替换文本内容 - (tp-set 1 6 '(face bold tp-text "Hi")) - ;; 文本变为 "Hi World",且 "Hi" 仍然有 bold 样式 - (message "buffer = %s" (buffer-string))) -;; => "Hi World" -``` - -### 响应式文本层 - -`tp-text` 的真正威力在于与响应式变量结合使用: - -```lisp -;; 定义响应式变量 -(defvar my-dynamic-text "Loading...") - -;; 定义包含 tp-text 的响应式层 -(define-tp dynamic-content () - :props '(face (:foreground "blue") tp-text $my-dynamic-text)) - -;; 应用到文本 -(tp-pop-to-buffer "*tp-test*" - (insert "placeholder") - (tp-set 1 12 'dynamic-content) - ;; 文本现在显示 "Loading..." - (message "初始文本: %s" (buffer-string)) - ;; => "Loading..." - - ;; 改变变量 - (setq my-dynamic-text "数据加载完成!") - ;; 文本自动更新! - (message "更新后: %s" (buffer-string))) -;; => "数据加载完成!" -``` - -### 使用 :compute 生成动态文本 - -`tp-text` 可以与 `:compute` 结合,创建由其他变量派生的动态文本: - -```lisp -(define-tp greeting-layer () - :props '(face (:foreground "green") tp-text $full-greeting) - :data '((user-name . "访客") - (greeting-prefix . "欢迎")) - :compute '((full-greeting - (lambda () - (format "%s, %s!" greeting-prefix user-name))))) - -;; 应用到文本 -(tp-pop-to-buffer "*tp-test*" - (insert "placeholder") - (tp-set 1 12 'greeting-layer) - ;; 显示 "欢迎, 访客!" - (message "初始: %s" (buffer-string)) - - ;; 改变用户名 - (setq user-name "张三") - ;; 文本自动更新为 "欢迎, 张三!" - (message "更新后: %s" (buffer-string))) -``` - -### 匿名响应式文本 - -你也可以直接在属性列表中使用响应式 `tp-text`,无需定义层: - -```lisp -(defvar inline-text "原始内容") - -(tp-pop-to-buffer "*tp-test*" - (insert "placeholder") - ;; 直接使用响应式 tp-text - (tp-set 1 12 '(face bold tp-text $inline-text)) - ;; 显示 "原始内容" - - ;; 改变变量 - (setq inline-text "新内容") - ;; 文本自动更新为 "新内容" - ) -``` - -### 注意事项 - -1. **tp-text 作用于字符串时返回新字符串**:Emacs 字符串长度无法原地改变,因此字符串形式的调用会返回一个新字符串,而不是修改原字符串。子区域的 `tp-text` 只替换该区域并保留字符串的其余部分;整串形式则只返回替换后的文本。 -2. **保留现有属性**:使用 `tp-set` 或 `tp-add` 设置 `tp-text` 时,现有的文本属性会被保留。 -3. **非响应式属性不添加 tp-name**:如果文本属性中没有响应式变量(`$` 前缀),则不会添加 `tp-name` 等响应式专用属性,保持原生文本属性行为。 - -## 使用 :transform 进行值转换 - -`:transform` 关键字允许你注册一个转换函数,在 `tp-text` 值显示之前对其进行处理。这对于格式化数字、日期或其他值非常有用: - -```lisp -;; 数字格式化 -(define-tp price-display () - :props '(tp-text $price) - :data '((price . "99.9")) - :transform (lambda (text) - (format "$%.2f" (string-to-number text)))) -;; 99.9 显示为 $99.90 - -;; 日期格式化 -(define-tp date-display () - :props '(tp-text $timestamp) - :data '((timestamp . "1703865600")) - :transform (lambda (text) - (format-time-string "%Y-%m-%d" - (seconds-to-time (string-to-number text))))) - -;; 大写转换 -(define-tp uppercase-text () - :props '(tp-text $content) - :data '((content . "hello")) - :transform #'upcase) -;; "hello" 显示为 "HELLO" -``` - -转换函数的特点: -- 接收原始的 `tp-text` 字符串值 -- 返回用于显示的转换后字符串 -- 在初始显示和响应式更新时都会应用 -- 必须返回字符串;错误或非字符串返回值会向上传播,避免继续显示陈旧结果 - -> 📖 **更多优化功能如批量更新和调试模式,请参阅 [响应式系统优化文档](reactive-optimization.md)** - -## 总结 - -tp.el 的响应式文本属性功能为 Emacs 开发带来了现代化的响应式编程体验。通过使用 `$` 前缀的响应式变量、`:data` 定义状态、`:compute` 计算派生值、`:watch` 监听变化、`:transform` 格式化值,你可以构建出更加动态、易于维护的文本属性系统。 - -核心要点: -1. **响应式变量**:使用 `$` 前缀引用变量 -2. **:props**:定义包含响应式变量的属性 -3. **:data**:定义额外的响应式状态和初始值 -4. **:compute**:定义由其他变量派生的计算属性 -5. **:watch**:监听变量变化并执行副作用 -6. **:transform**:在显示之前转换 tp-text 值 -7. **自动更新**:改变变量值,所有相关文本自动更新 -8. **响应式文本 (tp-text)**:让文本内容本身也能响应式更新 +TP 1.0 中,静态声明使用 direct property API/recipe,已有 host text 的响应式 +属性使用 tp-watch,TP 自己拥有的响应式文字使用 content surface。 diff --git a/docs/retained-runtime-target-architecture-en.md b/docs/retained-runtime-target-architecture-en.md index 9e395c7..8b1a087 100644 --- a/docs/retained-runtime-target-architecture-en.md +++ b/docs/retained-runtime-target-architecture-en.md @@ -2,6 +2,9 @@ Chinese version: [TP Retained/Reactive Text Runtime 目标架构](retained-runtime-target-architecture.md). +For the public entry index, see [API-REFERENCE.md](API-REFERENCE.md); for +behavior contracts, see [API-SEMANTICS.md](API-SEMANTICS.md). + Status: implemented TP 1.0 architecture contract. This document records the target boundaries now implemented and protected by tests; [ARCHITECTURE.md](ARCHITECTURE.md) and [API-SEMANTICS.md](API-SEMANTICS.md) are authoritative for current module and public-behavior facts. The `target-architecture` filename remains stable for existing links. ## 1. Product position @@ -191,7 +194,7 @@ With no Ebox on `load-path`, TP 1.0 continuously verifies: TP 1.0 is a major-version transition. Stateless public APIs that map directly to the unified core remain. Managed behavior that requires the inline `tp-layers`/`tp-meta` database or scan renderer is explicitly removed or replaced rather than retained in a hidden compatibility engine. -An explicit one-shot legacy import may scan historical propertized text and construct a surface. Normal signal/property/update hot paths never invoke it automatically. +There is no legacy import API in TP 1.0. Callers must provide a plan or producer explicitly; TP never scans historical propertized text to reconstruct surface identity. The current implementation has deleted the layer-to-buffer registry, scan-driven refresh hooks, old inline managed codec, duplicate transactions, and old batch renderer. The TP 1.0 package, tests, examples, and this document all work without an Ebox repository. diff --git a/docs/retained-runtime-target-architecture.md b/docs/retained-runtime-target-architecture.md index e2588d2..98bcf79 100644 --- a/docs/retained-runtime-target-architecture.md +++ b/docs/retained-runtime-target-architecture.md @@ -2,6 +2,9 @@ 英文版见 [TP Retained/Reactive Text Runtime Target Architecture](retained-runtime-target-architecture-en.md)。 +公共入口索引见 [API-REFERENCE.md](API-REFERENCE.md);语义合同见 +[API-SEMANTICS.md](API-SEMANTICS.md)。 + 状态:TP 1.0 已实现的架构合同。本文记录已经落地并由测试保护的目标边界;当前模块和公共行为事实分别以 [ARCHITECTURE.md](ARCHITECTURE.md) 与 [API-SEMANTICS.md](API-SEMANTICS.md) 为准。文件名保留 `target-architecture` 以维持既有链接稳定。 ## 1. 产品定位 @@ -191,7 +194,7 @@ TP 1.0 在没有 Ebox load-path 时必须长期通过: TP 1.0 是主版本切换。能直接映射到统一 core 的静态 public API 保留;依赖 inline `tp-layers`/`tp-meta` database 和扫描式 renderer 才能工作的 managed behavior 必须明确删除或替换,不保留 hidden compatibility engine。 -允许显式 one-shot legacy import 扫描历史 propertized text 并建立 surface;normal signal/property/update 热路径禁止自动调用它。 +当前没有 legacy import API;调用者必须显式提供 plan 或 producer。TP 不会扫描历史 propertized text 来重建 surface identity。 当前实现已经删除 layer→buffer registry、scan-driven refresh hooks、旧 inline managed codec、重复 transaction 和旧 batch renderer。TP 1.0 package、tests、examples 和本文档均可在没有 Ebox repository 的环境中独立工作。 diff --git a/tests/tp-core-tests.el b/tests/tp-core-tests.el index 9b7ffb0..70904d9 100644 --- a/tests/tp-core-tests.el +++ b/tests/tp-core-tests.el @@ -161,6 +161,132 @@ (should (equal (nth 0 copy) "value")) (should (equal (nth 1 copy) ["nested"]))))) +(ert-deftest tp-core-test-property-value-copy-keeps-list-functions-opaque () + "Property copies keep list-shaped function values opaque." + (let ((function-value '(lambda () 1))) + (should (functionp function-value)) + (should (eq (tp--copy-property-value function-value) + function-value)))) + +(ert-deftest tp-core-test-property-value-copy-deep-copies-string-properties () + "Property copies do not alias mutable values stored in string properties." + (let* ((owners (list 'original)) + (text (copy-sequence "x"))) + (add-text-properties 0 1 (list 'ebox-content-owners owners) text) + (let ((copy (tp--copy-property-value text))) + (should-not (eq copy text)) + (should-not (eq (get-text-property 0 'ebox-content-owners copy) + owners)) + (setcar owners 'caller-mutated) + (should (equal (get-text-property 0 'ebox-content-owners copy) + '(original))) + (setcar (get-text-property 0 'ebox-content-owners copy) 'copy-mutated) + (should (equal (get-text-property 0 'ebox-content-owners text) + '(caller-mutated)))))) + +(ert-deftest tp-core-test-property-value-copy-preserves-source-identity-rules () + "Property copies memoize one source identity without canonicalizing equals." + (let* ((shared (list 'shared)) + (equal-but-distinct (list 'shared)) + (value (list shared shared equal-but-distinct)) + (copy (tp--copy-property-value value))) + (should (eq (nth 0 copy) (nth 1 copy))) + (should-not (eq (nth 0 copy) (nth 2 copy))) + (should-not (eq (nth 0 copy) shared)) + (should-not (eq (nth 2 copy) equal-but-distinct)) + (setcar shared 'source-mutated) + (should (equal (nth 0 copy) '(shared))) + (setcar (nth 0 copy) 'copy-mutated) + (should (equal shared '(source-mutated))) + (should (equal equal-but-distinct '(shared))))) + +(ert-deftest tp-core-test-property-value-copy-reuses-string-identity () + "Property copies preserve repeated references to one propertized string." + (let* ((shared (copy-sequence "shared")) + (value (list shared shared)) + (copy (tp--copy-property-value value))) + (should (eq (nth 0 copy) (nth 1 copy))) + (should-not (eq (nth 0 copy) shared)))) + +(ert-deftest tp-core-test-text-snapshot-transfers-authorized-values () + "Text snapshots transfer only values explicitly owned by the caller." + (let* ((owned (list :face t)) + (copied (list :face t)) + (text (copy-sequence "ab"))) + (put-text-property 0 1 'face owned text) + (put-text-property 1 2 'payload copied text) + (let ((snapshot + (tp-text-snapshot + text (lambda (property value) + (and (eq property 'face) (eq value owned)))))) + (should (eq (get-text-property 0 'face snapshot) owned)) + (should-not (eq (get-text-property 1 'payload snapshot) copied)) + (should-not (eq (get-text-property 1 'payload snapshot) owned))))) + +(ert-deftest tp-core-test-text-snapshot-transfer-does-not-pollute-copy-cache () + "Transferred values do not make an unauthorized property alias its source." + (let* ((shared (list :face t)) + (text (copy-sequence "ab"))) + (put-text-property 0 1 'face shared text) + (put-text-property 1 2 'payload shared text) + (let ((snapshot + (tp-text-snapshot + text (lambda (property _value) (eq property 'face))))) + (should (eq (get-text-property 0 'face snapshot) shared)) + (should-not (eq (get-text-property 1 'payload snapshot) shared)) + (should-not (eq (get-text-property 1 'payload snapshot) + (get-text-property 0 'face snapshot)))))) + +(ert-deftest tp-core-test-text-snapshot-copies-propertized-strings-by-default () + "Text snapshots preserve nested string properties under strict copying." + (let* ((first (propertize "x" 'nested 'first)) + (second (propertize "x" 'nested 'second)) + (text (copy-sequence "ab"))) + (put-text-property 0 1 'payload first text) + (put-text-property 1 2 'payload second text) + (let ((snapshot (tp-text-snapshot text))) + (should-not (eq (get-text-property 0 'payload snapshot) + (get-text-property 1 'payload snapshot))) + (should (eq (get-text-property 0 'nested + (get-text-property 0 'payload snapshot)) + 'first)) + (should (eq (get-text-property 0 'nested + (get-text-property 1 'payload snapshot)) + 'second))))) + +(ert-deftest tp-core-test-text-snapshot-copies-circular-values-by-default () + "Text snapshots copy circular values without invoking equality." + (let* ((first (cons :cycle nil)) + (second (cons :cycle nil)) + (text (copy-sequence "ab"))) + (setcdr first first) + (setcdr second second) + (put-text-property 0 1 'payload first text) + (put-text-property 1 2 'payload second text) + (let ((snapshot (tp-text-snapshot text))) + (should-not (eq (get-text-property 0 'payload snapshot) + (get-text-property 1 'payload snapshot))) + (should (eq (cdr (get-text-property 0 'payload snapshot)) + (get-text-property 0 'payload snapshot))) + (should (eq (cdr (get-text-property 1 'payload snapshot)) + (get-text-property 1 'payload snapshot)))))) + +(ert-deftest tp-core-test-property-value-copy-preserves-flat-list-sharing () + "Flat-list snapshots memoize every cons cell, including shared tails." + (let* ((tail (list :tail)) + (value (list (cons :head tail) tail)) + (copy (tp--copy-property-value value))) + (should (eq (cdr (car copy)) (cadr copy))) + (should-not (eq (cadr copy) tail)))) + +(ert-deftest tp-core-test-property-value-copy-terminates-on-circular-list () + "Circular list snapshots use the recursive memo path without looping." + (let ((value (cons :cycle nil))) + (setcdr value value) + (let ((copy (tp--copy-property-value value))) + (should-not (eq copy value)) + (should (eq (cdr copy) copy))))) + ;;; API-COORD-01: ABSOLUTE coordinates in tp-intervals / tp-intervals-map (ert-deftest tp-core-test-intervals-buffer-relative-default () diff --git a/tests/tp-surface-tests.el b/tests/tp-surface-tests.el index 9550a09..944cef9 100644 --- a/tests/tp-surface-tests.el +++ b/tests/tp-surface-tests.el @@ -32,6 +32,16 @@ (tp-surface-test--leaf 'root (number-to-string (tp-signal-read signal))) (list :value (tp-signal-peek signal))))) +(defvar-local tp-surface-test--corrupt-next nil + "When non-nil, the test after-change hook corrupts one inserted range.") + +(defun tp-surface-test--corrupt-after-change (beg end _old-length) + "Corrupt one inserted range for publication rollback tests." + (when (and tp-surface-test--corrupt-next (< beg end)) + (setq-local tp-surface-test--corrupt-next nil) + (with-silent-modifications + (put-text-property beg end 'face 'corrupt)))) + (ert-deftest tp-surface-test-plan-validates-and-defensively-copies () "Plans reject duplicate keys and own their caller-provided values." (let* ((callback (byte-compile @@ -55,6 +65,156 @@ :capability 'content) :type 'tp-duplicate-object-key))) +(ert-deftest tp-surface-test-plan-snapshots-nested-values () + "Nested plan values remain defensive snapshots across producer boundaries." + (let* ((props (list 'face 'bold)) + (tags (list :role 'leaf)) + (text (copy-sequence "A")) + (child (tp-surface-plan-create + :key 'child :kind 'text :text text :props props :tags tags + :capability 'content)) + (children (list child)) + (parent (tp-surface-plan-create + :key 'root :kind 'group :children children + :capability 'content)) + (result (tp-surface-result-create parent)) + (normalized-from-result + (car (tp--producer-result result nil nil nil)))) + (tp-surface-test--with-buffer + (let* ((surface (tp--create-surface + buffer 'content '(:capability content))) + (normalized-from-plan + (car (tp--producer-result parent surface nil nil)))) + (let* ((result-plan (tp-surface-result-plan result)) + (result-child (car (tp-surface-plan-children result-plan)))) + (setf (tp-surface-plan-children result-plan) nil + (tp-surface-plan-text result-child) "result-mutated") + (setcar (tp-surface-plan-tags result-child) :result-mutated)) + (setcar props 'help-echo) + (setcar tags :mutated) + (aset text 0 ?Z) + (setcar children nil) + (setf (tp-surface-plan-text + (car (tp-surface-plan-children parent))) + "mutated") + (should-not (eq (car (tp-surface-plan-children parent)) child)) + (should-not (eq (tp-surface-result-plan result) parent)) + (should-not (eq normalized-from-result parent)) + (should-not (eq normalized-from-plan parent)) + (let ((rendered (tp-surface-materialize-string normalized-from-result))) + (should (equal (substring-no-properties rendered) "A")) + (should (eq (get-text-property 0 'face rendered) 'bold))) + (should (equal (tp-surface-plan-tags child) '(:role leaf))))))) + +(ert-deftest tp-surface-test-owned-plan-result-transfers-candidate-tree () + "Owned plan and result constructors skip a duplicate candidate snapshot." + (tp-surface-test--with-buffer + (let* ((surface (tp--create-surface + buffer 'content '(:capability content))) + (context (tp--make-context surface))) + (let ((tp--current-prepare-context context)) + (let* ((text (copy-sequence "owned")) + (child (tp-surface-plan-create-owned + :key 'child :kind 'text :text text + :capability 'content)) + (parent (tp-surface-plan-create-owned + :key 'root :kind 'group :children (list child) + :capability 'content)) + (result (tp-surface-result-create-owned context parent)) + (copies 0) + (original (symbol-function 'tp--copy-surface-plan))) + (cl-letf (((symbol-function 'tp--copy-surface-plan) + (lambda (&rest arguments) + (cl-incf copies) + (apply original arguments)))) + (should (eq (car (tp--producer-result result surface nil context)) + parent))) + (should (= copies 0)) + (should (tp-surface-result-plan-owned-p result)) + (should (tp-surface-result-consumed-p result)) + (should-not (tp-surface-result-plan result)) + (should-not (tp-surface-result-client-state result)) + (should-not (tp-surface-result-owner-context result)) + (should-error + (tp--producer-result result surface nil context) + :type 'tp-owned-result-error) + (should (eq (tp-surface-plan-text child) text))))))) + +(ert-deftest tp-surface-test-owned-result-rejects-inactive-or-wrong-context () + "Rejected owned results remain unconsumed for inactive or wrong contexts." + (tp-surface-test--with-buffer + (let* ((surface (tp--create-surface + buffer 'content '(:capability content))) + (context (tp--make-context surface)) + (other-context (tp--make-context surface)) + (plan (tp-surface-plan-create-owned + :key 'root :kind 'text :text "owned" + :capability 'content))) + (let ((tp--current-prepare-context context)) + (let ((result (tp-surface-result-create-owned context plan))) + (setf (tp--context-active context) nil) + (should-error + (tp--producer-result + result surface nil context) + :type 'tp-owned-result-error) + (should-not (tp-surface-result-consumed-p result)) + (should (eq (tp-surface-result-plan result) plan)) + (setf (tp--context-active context) t) + (let ((tp--current-prepare-context other-context)) + (should-error + (tp--producer-result result surface nil other-context) + :type 'tp-owned-result-error)) + (should-not (tp-surface-result-consumed-p result))))))) + +(ert-deftest tp-surface-test-direct-plan-prepare-snapshots-once () + "Direct plan prepare does not snapshot its already-copied plan twice." + (let ((plan (tp-surface-test--leaf 'root "direct")) + (copies 0) + (original (symbol-function 'tp--copy-surface-plan))) + (cl-letf (((symbol-function 'tp--copy-surface-plan) + (lambda (&rest args) + (cl-incf copies) + (apply original args)))) + (should (equal (substring-no-properties + (tp-surface-materialize-string plan)) + "direct")) + (should (= copies 1))))) + +(ert-deftest tp-surface-test-render-propagation-reuses-prepared-values () + "Rendering a prepared plan does not recursively copy inherited properties." + (let* ((inherited-value (list :space :width 1)) + (parent (list 'display inherited-value)) + (child (list 'face 'bold)) + (render-copies 0) + (copy-function (symbol-function 'tp--copy-property-value))) + (cl-letf (((symbol-function 'tp--copy-property-value) + (lambda (&rest arguments) + (cl-incf render-copies) + (apply copy-function arguments)))) + (let ((result (tp--plist-overlay parent child t))) + (should (equal (plist-get result 'face) 'bold)) + (should (eq (plist-get result 'display) inherited-value)) + (should-not (eq result parent)))) + (should (= render-copies 0)))) + +(ert-deftest tp-surface-test-owned-render-transfers-property-values () + "Owned producer plans transfer candidate property values to rendered text." + (let (candidate-value) + (let ((rendered + (tp-surface-materialize-string + (lambda (context) + (setq candidate-value (list :space :width 1)) + (tp-object-ensure context nil 'root 'text) + (tp-surface-result-create-owned + context + (tp-surface-plan-create-owned + :key 'root :kind 'text :text "owned" + :props (list 'display candidate-value) + :capability 'content)))))) + (should (equal (substring-no-properties rendered) "owned")) + (should (eq (get-text-property 0 'display rendered) + candidate-value))))) + (ert-deftest tp-surface-test-plan-copy-obeys-value-identity-rules () "Plan data is copied while opaque records and functions keep identity." (with-temp-buffer @@ -82,6 +242,31 @@ (should (eq (nth 6 copy) (current-buffer))) (should (= calls 0))))) +(ert-deftest tp-surface-test-plan-copy-shares-source-identity-across-children () + "One plan snapshot reuses one copy for repeated source identities globally." + (let* ((shared (list :shared t)) + (equal-but-distinct (list :shared t)) + (child-a (tp--make-surface-plan + :key 'a :kind 'text :tags (list shared))) + (child-b (tp--make-surface-plan + :key 'b :kind 'text + :tags (list shared equal-but-distinct))) + (plan (tp--make-surface-plan + :key 'root :kind 'group + :children (list child-a child-b))) + (copy (tp--copy-surface-plan plan)) + (copy-a + (tp-surface-plan-tags + (car (tp-surface-plan-children copy)))) + (copy-b + (tp-surface-plan-tags + (cadr (tp-surface-plan-children copy))))) + (should (eq (car copy-a) (car copy-b))) + (should-not (eq (car copy-b) (cadr copy-b))) + (should-not (eq (car copy-b) equal-but-distinct)) + (should-not (eq (cadr copy-b) equal-but-distinct)) + (should-not (eq (car copy-a) shared)))) + (ert-deftest tp-surface-test-retained-options-own-mutable-containers () "Surface options copy data containers without cloning opaque identities." (tp-surface-test--with-buffer @@ -149,6 +334,23 @@ (should (equal (nth 0 second-payload) "report")) (should (equal (nth 1 second-payload) ["nested"])))))) +(ert-deftest tp-surface-test-report-summary-contains-only-commit-scalars () + "Report summary exposes Ebox metrics without exposing nested report data." + (let ((surface + (tp--make-surface + :report '(:transaction-id 7 :text-operations 2 + :property-operations 3 :full-root nil + :scope-count 4 :scope-range-count 5 + :scope-fallback t :reconciled-objects 6 + :created-objects 7 :removed-objects 8 :moved-objects 9 + :payload (:mutable value))))) + (should (equal (tp-surface-report-summary surface) + '(:transaction-id 7 :text-operations 2 + :property-operations 3 :full-root nil + :scope-count 4 :scope-range-count 5 + :scope-fallback t :reconciled-objects 6 + :created-objects 7 :removed-objects 8 :moved-objects 9))))) + (ert-deftest tp-surface-test-materialize-producer-is-ephemeral () "Pure materialization leaves no live object, binding, or subscription." (let ((signal (tp-signal-create 7)) object binding) @@ -183,6 +385,284 @@ (should (eq (get-text-property 1 'face buffer) 'italic)) (should (= (plist-get (tp-surface-report surface) :text-operations) 1))))) +(ert-deftest tp-surface-test-coordinate-content-mounts-keep-public-ranges () + "Coordinate content mounts avoid marker churn without changing the API." + (tp-surface-test--with-buffer + (let* ((surface + (tp-surface-mount + buffer (tp-surface-test--leaf 'root "abc") + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root))) + (mount (car (gethash object (tp--surface-mount-index surface))))) + (should (integerp (tp--surface-mount-start mount))) + (should (integerp (tp--surface-mount-end mount))) + (should (tp-object-mounted-p object)) + (should (equal (car (tp-object-mounts object)) + '(:start 1 :end 4 :tags nil))) + (tp-surface-update surface (tp-surface-test--leaf 'root "abcd")) + (should (equal (car (tp-object-mounts object)) + '(:start 1 :end 5 :tags nil)))))) + +(ert-deftest tp-surface-test-batched-content-ranges-preserve-order () + "Batched content ranges retain attachment order and caller isolation." + (tp-surface-test--with-buffer + (let ((tags (list :slot 'first))) + (let* ((producer + (lambda (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-ranges + context object + (list (list :object object :start 0 :end 1 :tags tags) + (list :object object :start 1 :end 3 + :tags '(:slot second)))) + (tp-surface-plan-create + :key 'root :kind 'text :text "abc" + :capability 'content)))) + (surface (tp-surface-mount + buffer producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root)))) + (setcar tags :mutated) + (should (equal (tp-object-mounts object) + '((:start 1 :end 4 :tags nil) + (:start 1 :end 2 :tags (:slot first)) + (:start 2 :end 4 :tags (:slot second))))))))) + +(ert-deftest tp-surface-test-batched-content-ranges-reuse-identical-tags () + "A bulk attach shares one snapshot when the same TAGS object is reused." + (tp-surface-test--with-buffer + (let ((tags (list :slot 'same))) + (let* ((producer + (lambda (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-ranges + context object + (list (list :object object :start 0 :end 1 + :tags tags) + (list :object object :start 1 :end 2 + :tags tags))) + (tp-surface-plan-create + :key 'root :kind 'text :text "ab" + :capability 'content)))) + (surface (tp-surface-mount + buffer producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root))) + (tagged + (cl-remove-if-not + (lambda (mount) + (equal (tp--surface-mount-tags mount) '(:slot same))) + (gethash object (tp--surface-mount-index surface))))) + (should (= (length tagged) 2)) + (should (eq (tp--surface-mount-tags (car tagged)) + (tp--surface-mount-tags (cadr tagged)))) + (let ((public-tags (plist-get (cadr (tp-object-mounts object)) + :tags))) + (setcar public-tags :public-mutated) + (should (equal (plist-get (cadr (tp-object-mounts object)) :tags) + '(:slot same)))) + (setcar tags :mutated) + (should (equal (tp-object-mounts object) + '((:start 1 :end 3 :tags nil) + (:start 1 :end 2 :tags (:slot same)) + (:start 2 :end 3 :tags (:slot same))))))))) + +(ert-deftest tp-surface-test-batched-content-ranges-keep-opaque-tags-separate () + "Equal opaque tag values keep independent bulk snapshots." + (tp-surface-test--with-buffer + (let ((left-tags (copy-sequence "same")) + (right-tags (copy-sequence "same"))) + (let* ((producer + (lambda (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-ranges + context object + (list (list :object object :start 0 :end 1 + :tags left-tags) + (list :object object :start 1 :end 2 + :tags right-tags))) + (tp-surface-plan-create + :key 'root :kind 'text :text "ab" + :capability 'content)))) + (surface (tp-surface-mount + buffer producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root))) + (tagged + (cl-remove-if-not + (lambda (mount) + (stringp (tp--surface-mount-tags mount))) + (gethash object (tp--surface-mount-index surface))))) + (should (= (length tagged) 2)) + (should-not (eq (tp--surface-mount-tags (car tagged)) + (tp--surface-mount-tags (cadr tagged)))) + (aset left-tags 0 ?X) + (should (equal (mapcar (lambda (mount) + (tp--surface-mount-tags mount)) + tagged) + '("same" "same"))))))) + +(ert-deftest tp-surface-test-singular-content-range-snapshots-each-call () + "Separate singular attachments snapshot mutable tags independently." + (tp-surface-test--with-buffer + (let ((tags (list :slot 'before))) + (let* ((producer + (lambda (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-range + context object object 0 1 tags) + (setcar (cdr tags) 'after) + (tp-object-attach-content-range + context object object 1 2 tags) + (tp-surface-plan-create + :key 'root :kind 'text :text "abc" + :capability 'content)))) + (surface (tp-surface-mount + buffer producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root)))) + (should (equal (tp-object-mounts object) + '((:start 1 :end 4 :tags nil) + (:start 1 :end 2 :tags (:slot before)) + (:start 2 :end 3 :tags (:slot after))))))))) + +(ert-deftest tp-surface-test-owned-batched-content-ranges-transfer-tags () + "Owned bulk ranges retain freshly allocated TAGS without another copy." + (tp-surface-test--with-buffer + (let ((tags (list :slot 'owned))) + (let* ((producer + (lambda (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-ranges-owned + context object + (list (list :object object :start 0 :end 1 :tags tags))) + (tp-surface-plan-create + :key 'root :kind 'text :text "a" + :capability 'content)))) + (surface (tp-surface-mount + buffer producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root))) + (mount (cadr (tp-object-mounts object)))) + (should (eq tags (tp--surface-mount-tags + (cadr (gethash object + (tp--surface-mount-index surface)))))) + (should (equal (plist-get mount :tags) '(:slot owned))))))) + +(ert-deftest tp-surface-test-owned-batched-content-ranges-reuse-tags-object () + "Owned bulk ranges reuse the same transferred TAGS object." + (tp-surface-test--with-buffer + (let ((tags (list :slot 'owned-same))) + (let* ((producer + (lambda (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-ranges-owned + context object + (list (list :object object :start 0 :end 1 :tags tags) + (list :object object :start 1 :end 2 :tags tags))) + (tp-surface-plan-create + :key 'root :kind 'text :text "ab" + :capability 'content)))) + (surface (tp-surface-mount + buffer producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root))) + (tagged + (cl-remove-if-not + (lambda (mount) + (equal (tp--surface-mount-tags mount) + '(:slot owned-same))) + (gethash object (tp--surface-mount-index surface))))) + (should (= (length tagged) 2)) + (should (eq (tp--surface-mount-tags (car tagged)) tags)) + (should (eq (tp--surface-mount-tags (car tagged)) + (tp--surface-mount-tags (cadr tagged)))))))) + +(ert-deftest tp-surface-test-owned-batched-content-ranges-roll-back-tags () + "A failed owned range publication restores the previous live TAGS." + (tp-surface-test--with-buffer + (let ((text "a") + (tags (list :slot 'live))) + (cl-labels + ((producer + (context) + (let ((object (tp-object-ensure context nil 'root 'text))) + (tp-object-attach-content-ranges-owned + context object + (list (list :object object :start 0 :end 1 :tags tags))) + (tp-surface-plan-create + :key 'root :kind 'text :text text :capability 'content)))) + (let* ((surface (tp-surface-mount + buffer #'producer + '(:capability content :coordinate-mounts t))) + (object (tp-object-resolve surface '(root))) + (revision (tp-surface-revision surface)) + (live-tags + (tp--surface-mount-tags + (cadr (gethash object + (tp--surface-mount-index surface))))) + (candidate-tags (list :slot 'candidate))) + (setq text "b" + tags candidate-tags) + (let ((tp--surface-publication-step-function + (lambda (step _surface) + (when (eq step 'client-state) + (error "Injected owned range rollback"))))) + (should-error (tp-surface-update surface #'producer))) + (should (= (tp-surface-revision surface) revision)) + (should (equal (buffer-string) "a")) + (should (eq object (tp-object-resolve surface '(root)))) + (should + (eq live-tags + (tp--surface-mount-tags + (cadr (gethash object + (tp--surface-mount-index surface))))))))))) + +(ert-deftest tp-surface-test-content-publication-preserves-point () + "Content publication writes properties without losing the point." + (tp-surface-test--with-buffer + (let* ((surface + (tp-surface-mount + buffer (tp-surface-test--leaf 'root "old value" '(face bold)) + '(:capability content))) + (report + (progn + (goto-char 6) + (tp-surface-update + surface (tp-surface-test--leaf 'root "new value" '(face italic)))))) + (should (equal (buffer-string) "new value")) + (should (eq (get-text-property 1 'face buffer) 'italic)) + (should (= (point) 6)) + (should (= (plist-get report :text-operations) 1)) + (should (= (plist-get report :property-operations) 1))))) + +(ert-deftest tp-surface-test-content-publication-reapplies-hook-corruption () + "A hook that changes inserted properties must be corrected before commit." + (tp-surface-test--with-buffer + (let (surface before revision) + (add-hook 'after-change-functions + #'tp-surface-test--corrupt-after-change nil t) + (unwind-protect + (progn + (setq surface + (tp-surface-mount + buffer (tp-surface-test--leaf 'root "old" '(face bold)) + '(:capability content))) + (setq before (buffer-substring 1 4) + revision (tp-surface-revision surface) + tp-surface-test--corrupt-next t) + (tp-surface-update + surface (tp-surface-test--leaf 'root "new" '(face italic))) + (should (equal (buffer-string) "new")) + (should (= (tp-surface-revision surface) (1+ revision))) + (should (eq (get-text-property 1 'face buffer) 'italic)) + (should-not (equal-including-properties (buffer-substring 1 4) + before))) + (when (and surface (tp-surface-live-p surface)) + (tp-surface-unmount surface)) + (remove-hook 'after-change-functions + #'tp-surface-test--corrupt-after-change t))))) + (ert-deftest tp-surface-test-scoped-content-update-publishes-one-object () "A scoped update should replace only its retained object's mounted text." (tp-surface-test--with-buffer @@ -214,6 +694,67 @@ (should (= (plist-get report :scope-range-count) 1)) (should (= (plist-get report :touched-characters) 4))))))) +(ert-deftest tp-surface-test-scoped-update-can-skip-report-snapshot () + "A scoped caller may skip an unused defensive report snapshot." + (tp-surface-test--with-buffer + (let ((middle "B") middle-object) + (let* ((producer + (lambda (context) + (let ((root (tp-object-ensure context nil 'root 'group))) + (setq middle-object + (tp-object-ensure context root 'middle 'text))) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children (list (tp-surface-test--leaf 'middle middle))))) + (surface (tp-surface-mount buffer producer '(:capability content))) + (copies 0) + (original (symbol-function 'tp-surface-report))) + (setq middle "LONG") + (cl-letf (((symbol-function 'tp-surface-report) + (lambda (&rest arguments) + (cl-incf copies) + (apply original arguments)))) + (should-not + (tp-surface-update-scoped + surface (list middle-object) producer '(:return-report nil)))) + (should (= copies 0)) + (should (= (tp-surface-revision surface) 2)))))) + +(ert-deftest tp-surface-test-scoped-content-property-only-keeps-text-stable () + "A scoped property-only change must not replace its character range." + (tp-surface-test--with-buffer + (let ((middle-face 'bold) middle-object) + (let* ((producer + (lambda (context) + (let ((root (tp-object-ensure context nil 'root 'group))) + (tp-object-ensure context root 'left 'text) + (setq middle-object + (tp-object-ensure context root 'middle 'text)) + (tp-object-ensure context root 'right 'text)) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children + (list (tp-surface-test--leaf 'left "A") + (tp-surface-test--leaf 'middle "B" + (list 'face middle-face)) + (tp-surface-test--leaf 'right "C"))))) + (surface + (tp-surface-mount buffer producer '(:capability content))) + (marker (copy-marker 3 t))) + (setq middle-face 'italic) + (cl-letf (((symbol-function 'delete-region) + (lambda (&rest _) + (error "Scoped property update replaced text")))) + (let ((report + (tp-surface-update-scoped + surface (list middle-object) producer))) + (should (= (plist-get report :text-operations) 0)) + (should (> (plist-get report :property-operations) 0)))) + (should (equal (buffer-string) "ABC")) + (should (eq (get-text-property 2 'face) 'italic)) + (should (= (marker-position marker) 3)) + (set-marker marker nil))))) + (ert-deftest tp-surface-test-scoped-update-supports-disjoint-object-mounts () "One logical scope should update all of its disjoint mounts atomically." (tp-surface-test--with-buffer @@ -550,6 +1091,143 @@ '((:start 1 :end 3 :tags (:slot left)) (:start 3 :end 6 :tags (:slot right))))))))) +(ert-deftest tp-surface-test-retain-subtree-preserves-omitted-objects-and-bindings () + "Retaining a live subtree preserves its objects and default bindings." + (tp-surface-test--with-buffer + (let ((value "before") logical child binding) + (cl-labels + ((producer (context) + (let ((root (tp-object-ensure context nil 'root 'group))) + (if logical + (tp-object-retain-subtree context logical) + (setq logical + (tp-object-ensure context root 'logical 'group) + child + (tp-object-ensure context logical 'child 'item) + binding + (tp-bind logical '(test . value) + (lambda () value))) + (tp-object-retain context logical) + (tp-object-retain context child)) + (tp-object-ensure context root 'text 'text) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children (list (tp-surface-test--leaf 'text value)))))) + (let* ((surface (tp-surface-mount + buffer #'producer '(:capability content))) + (revision (tp-surface-revision surface))) + (setq value "after") + (tp-surface-update surface #'producer) + (should (= (tp-surface-revision surface) (1+ revision))) + (should (eq logical (tp-object-resolve surface '(root logical)))) + (should (eq child (tp-object-resolve + surface '(root logical child)))) + (should (tp-binding-live-p binding)) + (should (equal (buffer-string) "after"))))))) + +(ert-deftest tp-surface-test-reuse-subtree-skips-unchanged-reconcile () + "Reusing a proven subtree keeps its objects without reconciling them." + (tp-surface-test--with-buffer + (let ((value "before") logical child binding) + (cl-labels + ((producer (context) + (let ((root (tp-object-ensure context nil 'root 'group))) + (if logical + (tp-object-reuse-subtree context logical) + (setq logical + (tp-object-ensure context root 'logical 'group) + child + (tp-object-ensure context logical 'child 'item) + binding + (tp-bind logical '(test . value) + (lambda () value))) + (tp-object-retain context logical) + (tp-object-retain context child)) + (tp-object-ensure context root 'text 'text)) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children (list (tp-surface-test--leaf 'text value))))) + (let* ((surface (tp-surface-mount + buffer #'producer '(:capability content))) + (revision (tp-surface-revision surface))) + (setq value "after") + (tp-surface-update surface #'producer) + (should (= (tp-surface-revision surface) (1+ revision))) + (should (= (plist-get (tp-surface-report surface) + :reconciled-objects) + 2)) + (should (eq logical (tp-object-resolve surface '(root logical)))) + (should (eq child (tp-object-resolve + surface '(root logical child)))) + (should (tp-binding-live-p binding)) + (should (equal (buffer-string) "after"))))))) + +(ert-deftest tp-surface-test-logical-object-owns-leaf-local-ranges () + "One logical object can own multiple ranges in one content leaf." + (tp-surface-test--with-buffer + (let* ((producer + (lambda (context) + (let* ((root (tp-object-ensure context nil 'root 'group)) + (left (tp-object-ensure context root 'left 'item)) + (right (tp-object-ensure context root 'right 'item)) + (leaf (tp-object-ensure context root 'leaf 'text))) + (tp-object-attach-content-range + context left leaf 0 1 '(:slot left)) + (tp-object-attach-content-range + context left leaf 3 4 '(:slot right)) + (tp-object-attach-content-range + context right leaf 1 3 '(:slot middle))) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children + (list (tp-surface-plan-create + :key 'leaf :kind 'text :text "ABCD" + :capability 'content))))) + (surface (tp-surface-mount + buffer producer '(:capability content))) + (left (tp-object-resolve surface '(root left))) + (right (tp-object-resolve surface '(root right)))) + (should (equal (buffer-string) "ABCD")) + (should (equal (tp-object-mounts left) + '((:start 1 :end 2 :tags (:slot left)) + (:start 4 :end 5 :tags (:slot right))))) + (should (equal (tp-object-mounts right) + '((:start 2 :end 4 :tags (:slot middle)))))))) + +(ert-deftest tp-surface-test-leaf-local-ranges-validate-before-publication () + "Leaf-local ranges reject malformed and out-of-bounds attachments." + (tp-surface-test--with-buffer + (let ((producer + (lambda (context) + (let* ((root (tp-object-ensure context nil 'root 'group)) + (owner (tp-object-ensure context root 'owner 'item)) + (leaf (tp-object-ensure context root 'leaf 'text))) + (tp-object-attach-content-range + context owner leaf 0 0 '(:slot empty))) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children + (list (tp-surface-plan-create + :key 'leaf :kind 'text :text "AB" + :capability 'content)))))) + (should-error (tp-surface-mount buffer producer '(:capability content)) + :type 'tp-invalid-content-range) + (let ((producer + (lambda (context) + (let* ((root (tp-object-ensure context nil 'root 'group)) + (owner (tp-object-ensure context root 'owner 'item)) + (leaf (tp-object-ensure context root 'leaf 'text))) + (tp-object-attach-content-range + context owner leaf 0 3 '(:slot too-long))) + (tp-surface-plan-create + :key 'root :kind 'group :capability 'content + :children + (list (tp-surface-plan-create + :key 'leaf :kind 'text :text "AB" + :capability 'content)))))) + (should-error (tp-surface-mount buffer producer '(:capability content)) + :type 'tp-invalid-content-range))))) + (ert-deftest tp-surface-test-explicitly-retained-object-may-be-unmounted () "An explicitly retained logical object may have no rendered characters." (tp-surface-test--with-buffer @@ -742,6 +1420,16 @@ (when (and surface (tp-surface-live-p surface)) (tp-surface-unmount surface)))))) +(ert-deftest tp-surface-test-content-property-diff-detects-interior-boundary () + "Content property diffing notices a mismatch after a matching first char." + (tp-surface-test--with-buffer + (insert "abc") + (put-text-property 1 2 'face 'bold) + (let ((rendered (copy-sequence "abc"))) + (put-text-property 0 3 'face 'bold rendered) + (should (tp--string-property-run-diff-p + buffer 1 rendered 0 3))))) + (ert-deftest tp-surface-test-unmount-preserves-conflicting-host-value () "Unmount removes only TP's still-current property contribution." (tp-surface-test--with-buffer @@ -902,15 +1590,20 @@ '(:capability content))) (revision (tp-surface-revision surface)) (object (tp-object-resolve surface '(root))) + (reached nil) + (producer + (lambda (context) + (tp-object-ensure context nil 'root 'text) + (tp-surface-result-create + (tp-surface-test--leaf 'root "new" '(face bold)) + (list :candidate step)))) (tp--surface-publication-step-function (lambda (current _surface) + (push current reached) (when (eq current step) (error "Injected %s failure" step))))) (should-error - (tp-surface-update - surface - (tp-surface-result-create - (tp-surface-test--leaf 'root "new" '(face bold)) - (list :candidate step)))) + (tp-surface-update surface producer)) + (should (memq step reached)) (should (equal-including-properties (buffer-string) "old")) (should (= (tp-surface-revision surface) revision)) (should (eq object (tp-object-resolve surface '(root)))))))) @@ -1102,6 +1795,20 @@ "x")) (tp-surface-unmount surface)))) +(ert-deftest tp-surface-test-no-observer-skips-report-snapshot () + "A surface without observers does not copy an unused asynchronous report." + (tp-surface-test--with-buffer + (let ((copies 0) + (original (symbol-function 'tp-surface-report))) + (cl-letf (((symbol-function 'tp-surface-report) + (lambda (&rest arguments) + (cl-incf copies) + (apply original arguments)))) + (tp-surface-mount + buffer (tp-surface-test--leaf 'root "committed") + '(:capability content))) + (should (= copies 0))))) + (ert-deftest tp-surface-test-observer-failure-does-not-roll-back () "Observer errors are recorded after a successful publication." (tp-surface-test--with-buffer diff --git a/tp-core.el b/tp-core.el index 4cb5a52..630ff5d 100644 --- a/tp-core.el +++ b/tp-core.el @@ -271,24 +271,159 @@ Otherwise, START-OR-STRING and END define the range." do (setq result (plist-put result key val))))) result))) -(defun tp--copy-property-value (value) +(defconst tp--copy-cache-missing (make-symbol "tp-copy-cache-missing") + "Sentinel for a missing defensive-copy memo entry.") + +(defun tp--copy-mutable-property-value-p (value) + "Return non-nil when VALUE is a recursively copied property container." + (and (not (functionp value)) + (or (consp value) + (stringp value) + (and (vectorp value) (not (recordp value)))))) + +(defun tp--copy-cons-spine (value cache) + "Return a single-pass memoized copy of cons spine VALUE using CACHE. +Each cons cell is allocated and entered into CACHE before its car is copied, +so dotted tails, shared suffixes, and cycles retain their source topology." + (let ((source value) + head + tail + reused-p) + (while (consp source) + (let ((cached (gethash source cache tp--copy-cache-missing))) + (if (not (eq cached tp--copy-cache-missing)) + (progn + (if tail + (setcdr tail cached) + (setq head cached)) + (setq reused-p t + source nil)) + (let ((copy (cons nil nil))) + (puthash source copy cache) + (if tail + (setcdr tail copy) + (setq head copy)) + (let ((item (car source))) + (setcar + copy + (if (and (not (functionp item)) + (or (consp item) + (stringp item) + (and (vectorp item) (not (recordp item))))) + (tp--copy-property-value item cache) + item))) + (setq tail copy + source (cdr source)))))) + (when (and tail (not reused-p)) + (setcdr tail + (if (null source) + nil + (tp--copy-property-value source cache)))) + head)) + +(defun tp--copy-proper-cons-list (value cache) + "Return a fast memoized copy of uncached proper-list VALUE. +The whole spine is registered before mutable cars are copied, preserving +back-references from cars while `copy-sequence' supplies the spine cheaply." + (let ((source value) + (cache-free-p t)) + (while (and cache-free-p (consp source)) + (when (not (eq (gethash source cache tp--copy-cache-missing) + tp--copy-cache-missing)) + (setq cache-free-p nil)) + (setq source (cdr source))) + (if (not cache-free-p) + (tp--copy-cons-spine value cache) + (let* ((copy (copy-sequence value)) + (source value) + (target copy)) + (while (consp source) + (puthash source target cache) + (let ((item (car source))) + (when (and (not (functionp item)) + (or (consp item) + (stringp item) + (and (vectorp item) (not (recordp item))))) + (setcar target (tp--copy-property-value item cache)))) + (setq source (cdr source) + target (cdr target))) + copy)))) + +(defun tp--copy-string-with-properties + (value &optional cache reuse-property-p) + "Return a copy of string VALUE with recursively copied property values. +REUSE-PROPERTY-P, when non-nil, is called with PROPERTY and VALUE. A non-nil +result transfers that exact candidate-owned VALUE into the returned string; +the caller must ensure that VALUE is not mutated by another owner. Values not +transferred keep strict source-identity memoization and recursive copying." + (let* ((cache (or cache (make-hash-table :test #'eq))) + (cached (gethash value cache tp--copy-cache-missing))) + (if (not (eq cached tp--copy-cache-missing)) + cached + (let ((copy (copy-sequence value)) + (position 0) + (length (length value))) + (puthash value copy cache) + (while (< position length) + (let ((next (or (next-property-change position value) length)) + (properties (text-properties-at position value)) + updates) + (while properties + (let* ((property (pop properties)) + (original (pop properties)) + (mutable-p + (tp--copy-mutable-property-value-p original)) + (reuse-p + (and mutable-p reuse-property-p + (funcall reuse-property-p property original))) + (copied + (if reuse-p + original + (if mutable-p + (tp--copy-property-value original cache) + original)))) + (unless (eq copied original) + (push property updates) + (push copied updates)))) + (when updates + (add-text-properties position next (nreverse updates) copy)) + (setq position next))) + copy)))) + +(defun tp-text-snapshot (text &optional reuse-property-p) + "Return a defensive copy of TEXT and its mutable text-property values. +REUSE-PROPERTY-P, when non-nil, is called with PROPERTY and VALUE. A non-nil +result transfers that exact candidate-owned VALUE into the snapshot; the +caller must ensure that VALUE is not mutated by another owner. Without the +predicate, repeated source identities share defensive copies and all mutable +values are recursively isolated." + (unless (stringp text) + (signal 'wrong-type-argument (list 'stringp text))) + (tp--copy-string-with-properties text nil reuse-property-p)) + +(defun tp--copy-property-value (value &optional cache) "Return a defensive copy of mutable containers in property VALUE. Cons cells, strings, and vectors are copied recursively. Functions, records, and other opaque objects keep their identity; functions are never executed." - (cond - ((functionp value) value) - ((recordp value) value) - ((consp value) - (cons (tp--copy-property-value (car value)) - (tp--copy-property-value (cdr value)))) - ((stringp value) (copy-sequence value)) - ((vectorp value) - (let ((copy (copy-sequence value))) - (dotimes (index (length copy)) - (aset copy index - (tp--copy-property-value (aref copy index)))) - copy)) - (t value))) + (if (not (tp--copy-mutable-property-value-p value)) + value + (let* ((cache (or cache (make-hash-table :test #'eq))) + (cached (gethash value cache tp--copy-cache-missing))) + (if (not (eq cached tp--copy-cache-missing)) + cached + (cond + ((consp value) + (if (proper-list-p value) + (tp--copy-proper-cons-list value cache) + (tp--copy-cons-spine value cache))) + ((stringp value) (tp--copy-string-with-properties value cache)) + ((vectorp value) + (let ((copy (copy-sequence value))) + (puthash value copy cache) + (dotimes (index (length copy)) + (aset copy index + (tp--copy-property-value (aref copy index) cache))) + copy))))))) (defun tp--deep-merge-plist (base new) "Deep merge NEW plist into BASE plist. diff --git a/tp-surface.el b/tp-surface.el index 3fb13cb..310e2af 100644 --- a/tp-surface.el +++ b/tp-surface.el @@ -42,20 +42,31 @@ (define-error 'tp-dead-surface "Dead TP surface" 'tp-surface-error) (define-error 'tp-invalid-range-anchor "Invalid TP range anchor" 'tp-surface-error) +(define-error 'tp-invalid-content-range "Invalid TP content leaf range" + 'tp-surface-error) (define-error 'tp-producer-buffer-mutation "TP producer mutated a live surface buffer during prepare" 'tp-surface-error) +(define-error 'tp-publication-mismatch + "TP publication was changed by an external buffer hook" + 'tp-surface-error) (define-error 'tp-scope-mismatch "Scoped TP update changed outside its objects" 'tp-surface-error) +(define-error 'tp-owned-result-error "Invalid TP owned producer result" + 'tp-surface-error) (cl-defstruct (tp-surface-plan (:constructor tp--make-surface-plan)) "Pure retained node data accepted by a TP surface." key kind text props children tags capability) (cl-defstruct (tp-surface-result - (:constructor tp--make-surface-result (plan client-state))) - "A producer result carrying PLAN and opaque CLIENT-STATE." - plan client-state) + (:constructor tp--make-surface-result + (plan client-state &optional plan-owned-p + owner-context))) + "A producer result carrying PLAN and opaque CLIENT-STATE. +PLAN-OWNED-P marks an explicit candidate-local ownership transfer. The +owned result is bound to OWNER-CONTEXT and consumed once during prepare." + plan client-state plan-owned-p owner-context consumed-p) (cl-defstruct (tp-surface (:constructor tp--make-surface) @@ -76,7 +87,8 @@ (:conc-name tp--context-)) "Short-lived candidate identity and attachment owner." surface objects touched retained bindings attachments fragment-attachments - child-seen child-positions new-objects created-anchors active ephemeral) + content-range-attachments child-seen + child-positions new-objects created-anchors active ephemeral) (cl-defstruct (tp-range-anchor (:constructor tp--make-range-anchor) @@ -87,6 +99,10 @@ (cl-defstruct (tp--surface-mount (:constructor tp--make-surface-mount)) object start end tags capability anchor) +(defun tp--mount-position (position) + "Return numeric POSITION for a marker or coordinate mount endpoint." + (if (markerp position) (marker-position position) position)) + (cl-defstruct (tp--property-ledger (:constructor tp--make-property-ledger)) start end property baseline-present baseline-value published-present published-value anchors) @@ -110,6 +126,7 @@ (defvar tp--surface-publishing nil) (defvar tp--surface-guarding-prepare nil) (defvar tp--surface-publication-step-function nil) +(defvar tp--content-published-ranges nil) (defvar-local tp--buffer-surfaces nil) (defvar-local tp--surface-character-tick 0) @@ -148,22 +165,43 @@ (signal 'tp-duplicate-object-key (list key))) (puthash key t seen))))) -(defun tp--copy-surface-plan (plan) - "Return a validated defensive copy of PLAN." +(defun tp--validate-surface-plan-tree (plan) + "Validate PLAN recursively without copying its caller-owned values." (unless (tp-surface-plan-p plan) (signal 'wrong-type-argument (list 'tp-surface-plan-p plan))) - (let* ((children (mapcar #'tp--copy-surface-plan - (tp-surface-plan-children plan))) - (kind (tp--copy-property-value (tp-surface-plan-kind plan))) - (text (tp--copy-property-value (tp-surface-plan-text plan))) - (props (tp--copy-property-value (tp-surface-plan-props plan))) + (let ((children (tp-surface-plan-children plan))) + (tp--validate-plan-fields + (tp-surface-plan-kind plan) + (tp-surface-plan-text plan) + (tp-surface-plan-props plan) + children + (tp-surface-plan-capability plan)) + (tp--validate-sibling-keys children) + (dolist (child children) + (tp--validate-surface-plan-tree child))) + plan) + +(defun tp--copy-surface-plan (plan &optional cache) + "Return a validated defensive snapshot of PLAN. +CACHE is shared by one top-level snapshot so repeated source identities retain +one copied identity across sibling nodes without canonicalizing equal values." + (unless (tp-surface-plan-p plan) + (signal 'wrong-type-argument (list 'tp-surface-plan-p plan))) + (let* ((cache (or cache (make-hash-table :test #'eq))) + (children + (mapcar (lambda (child) + (tp--copy-surface-plan child cache)) + (tp-surface-plan-children plan))) + (kind (tp--copy-property-value (tp-surface-plan-kind plan) cache)) + (text (tp--copy-property-value (tp-surface-plan-text plan) cache)) + (props (tp--copy-property-value (tp-surface-plan-props plan) cache)) (capability (tp-surface-plan-capability plan))) (tp--validate-plan-fields kind text props children capability) (tp--validate-sibling-keys children) (tp--make-surface-plan - :key (tp--copy-property-value (tp-surface-plan-key plan)) + :key (tp--copy-property-value (tp-surface-plan-key plan) cache) :kind kind :text text :props props :children children - :tags (tp--copy-property-value (tp-surface-plan-tags plan)) + :tags (tp--copy-property-value (tp-surface-plan-tags plan) cache) :capability capability))) (cl-defun tp-surface-plan-create @@ -177,9 +215,37 @@ plans, TAGS are opaque metadata, and CAPABILITY is `content' or `properties'." :key key :kind kind :text text :props props :children children :tags tags :capability capability))) +(cl-defun tp-surface-plan-create-owned + (&key key kind text props children tags capability) + "Create a candidate-local surface plan without copying its values. +KEY, KIND, TEXT, PROPS, CHILDREN, TAGS, and CAPABILITY have the same meanings +as in `tp-surface-plan-create'. The caller transfers ownership of those +values; after this call they must not be mutated or exposed to another owner. +Use `tp-surface-plan-create' for ordinary caller-owned values." + (tp--validate-surface-plan-tree + (tp--make-surface-plan + :key key :kind kind :text text :props props :children children + :tags tags :capability capability))) + (defun tp-surface-result-create (plan &optional client-state) - "Return a producer result containing PLAN and opaque CLIENT-STATE." - (tp--make-surface-result (tp--copy-surface-plan plan) client-state)) + "Return a producer result containing PLAN and opaque CLIENT-STATE. +TP always renders the submitted plan inside its prepare transaction. This +keeps the plan as the single authoritative source for published content." + (tp--make-surface-result + (tp--copy-surface-plan plan) client-state)) + +(defun tp-surface-result-create-owned (context plan &optional client-state) + "Create a one-shot result by adopting candidate-local PLAN. +CONTEXT must be the active prepare context that owns PLAN. PLAN and all +nested values must be freshly allocated for that candidate and must not be + mutated or exposed after this call. CLIENT-STATE is opaque producer state. +Use `tp-surface-result-create' for ordinary caller-owned plans." + (tp--validate-prepare-context context) + (unless (eq context tp--current-prepare-context) + (signal 'tp-owned-result-error + (list :context-not-current context tp--current-prepare-context))) + (tp--make-surface-result + (tp--validate-surface-plan-tree plan) client-state t context)) (defun tp--property-value-equal-p (property left right) "Return non-nil when PROPERTY values LEFT and RIGHT are policy-equal." @@ -274,7 +340,8 @@ plans, TAGS are opaque metadata, and CAPABILITY is `content' or `properties'." (let ((object (tp--make-surface-object :id (cl-incf tp--object-id-counter) :surface (tp--context-surface context) :parent parent - :key (tp--copy-property-value key) :kind kind :path path + :key (tp--copy-property-value key) + :kind kind :path path :candidate-context context))) (push object (tp--context-new-objects context)) object)) @@ -309,6 +376,17 @@ plans, TAGS are opaque metadata, and CAPABILITY is `content' or `properties'." (let ((object (gethash key-path (tp--surface-objects surface)))) (and (tp-object-live-p object) object))) +(defun tp-object-mounted-p (object) + "Return non-nil when live OBJECT owns at least one published mount. +This predicate does not copy mount tags; use `tp-object-mounts' when callers +need the public coordinate and metadata snapshots." + (unless (tp-object-live-p object) + (signal 'tp-stale-object (list object))) + (and (gethash object + (tp--surface-mount-index + (tp--surface-object-surface object))) + t)) + (defun tp-object-mounts (object) "Return OBJECT's live numeric mount ranges and opaque tags. Each result is a plist with `:start', `:end', and `:tags'. Marker objects @@ -319,8 +397,8 @@ remain private so callers cannot mutate TP's publication coordinates." (mounts (gethash object (tp--surface-mount-index surface)))) (mapcar (lambda (mount) - (list :start (marker-position (tp--surface-mount-start mount)) - :end (marker-position (tp--surface-mount-end mount)) + (list :start (tp--mount-position (tp--surface-mount-start mount)) + :end (tp--mount-position (tp--surface-mount-end mount)) :tags (tp--copy-property-value (tp--surface-mount-tags mount)))) mounts))) @@ -338,6 +416,7 @@ When EPHEMERAL is non-nil, no identity may be promoted." :bindings (make-hash-table :test #'eq) :attachments (make-hash-table :test #'eq) :fragment-attachments (make-hash-table :test #'eq) + :content-range-attachments (make-hash-table :test #'eq) :child-seen (make-hash-table :test #'eq) :child-positions (make-hash-table :test #'eq) :active t :ephemeral ephemeral)) @@ -348,7 +427,8 @@ When EPHEMERAL is non-nil, no identity may be promoted." (unless (and (tp-object-p object) (eq (tp--surface-object-surface object) (tp--context-surface context)) - (gethash object (tp--context-touched context))) + (or (gethash object (tp--context-touched context)) + (gethash object (tp--context-retained context)))) (signal 'tp-stale-object (list object)))) (defun tp-object-retain (context object) @@ -357,6 +437,71 @@ When EPHEMERAL is non-nil, no identity may be promoted." (puthash object t (tp--context-retained context)) object) +(defun tp--object-path-prefix-p (prefix path) + "Return non-nil when PREFIX is a path prefix of PATH." + (and (<= (length prefix) (length path)) + (equal prefix (cl-subseq path 0 (length prefix))))) + +(defun tp-object-retain-subtree (context root) + "Retain the existing object subtree rooted at live ROOT in CONTEXT. +ROOT must be a live object already owned by CONTEXT's surface. The operation +marks ROOT and every already-known descendant as touched and retained, and +also keeps their live bindings in the candidate lifecycle. It does not +create missing objects or prove that an omitted subtree is unchanged; the +producer must provide that proof and a complete candidate plan separately." + (tp--validate-prepare-context context) + (unless (tp-object-p root) + (signal 'wrong-type-argument (list 'tp-object-p root))) + (let* ((surface (tp--context-surface context)) + (path (tp--surface-object-path root)) + (objects (tp--context-objects context))) + (unless (and (tp-object-live-p root) + (eq root (gethash path objects))) + (signal 'tp-stale-object (list root))) + (maphash + (lambda (_candidate-path object) + (when (tp--object-path-prefix-p + path (tp--surface-object-path object)) + (unless (and (eq (tp--surface-object-surface object) surface) + (tp-object-live-p object)) + (signal 'tp-stale-object (list object))) + (puthash object t (tp--context-touched context)) + (puthash object t (tp--context-retained context)) + (dolist (binding (tp-binding-owner-bindings object)) + (when (tp-binding-live-p binding) + (puthash binding t (tp--context-bindings context)))))) + objects)) + root) + +(defun tp-object-reuse-subtree (context root) + "Reuse a proven unchanged live subtree in CONTEXT. +ROOT must be a live object already owned by CONTEXT's surface. The subtree's +objects and live bindings remain in the candidate, but are not marked touched +or reconciled. The caller must still provide a complete candidate plan and +prove that the subtree's identity, output, and lifecycle are unchanged." + (tp--validate-prepare-context context) + (unless (tp-object-p root) + (signal 'wrong-type-argument (list 'tp-object-p root))) + (let* ((surface (tp--context-surface context)) + (path (tp--surface-object-path root)) + (objects (tp--context-objects context))) + (unless (and (tp-object-live-p root) + (eq root (gethash path objects))) + (signal 'tp-stale-object (list root))) + (maphash + (lambda (_candidate-path object) + (when (tp--object-path-prefix-p + path (tp--surface-object-path object)) + (unless (and (eq (tp--surface-object-surface object) surface) + (tp-object-live-p object)) + (signal 'tp-stale-object (list object))) + (puthash object t (tp--context-retained context)) + (dolist (binding (tp-binding-owner-bindings object)) + (when (tp-binding-live-p binding) + (puthash binding t (tp--context-bindings context)))))) + objects)) + root) + (defun tp-object-attach-fragment (context object fragment &optional tags) "Attach logical OBJECT to output FRAGMENT with opaque TAGS in CONTEXT. OBJECT and FRAGMENT must be candidate handles from the same content surface. @@ -375,6 +520,87 @@ marker-backed mounts after publication." (tp--context-fragment-attachments context))) (tp-object-retain context object)) +(defun tp--validate-content-range (start end) + "Signal unless START and END describe a nonempty local leaf range." + (unless (and (integerp start) (integerp end) + (>= start 0) (> end start)) + (signal 'tp-invalid-content-range (list :start start :end end)))) + +(defun tp--copy-content-range-tags (tags &optional copies) + "Return a defensive copy of non-nil content-range TAGS. +COPIES, when non-nil, is a call-local `eq' cache owned by one bulk attach +operation. A singular attach always snapshots TAGS independently, so later +caller mutation cannot alter an earlier attachment." + (if (null tags) + nil + (let ((copy (and copies (gethash tags copies)))) + (or copy + (let ((copy (tp--copy-property-value tags))) + (when copies + (puthash tags copy copies)) + copy))))) + +(defun tp-object-attach-content-range + (context object leaf start end &optional tags) + "Attach OBJECT to [START, END) in candidate content LEAF for CONTEXT. +START and END are character offsets relative to LEAF's text, not buffer +positions. LEAF must be a candidate text leaf in the same content surface; +the range is validated against its rendered length during prepare. TAGS are +opaque metadata copied into the eventual marker-backed mount." + (tp--validate-context-object context object) + (tp--validate-context-object context leaf) + (tp--validate-content-range start end) + (let ((attachments + (gethash leaf (tp--context-content-range-attachments context)))) + (puthash leaf + (cons (list :object object :start start :end end + :tags (tp--copy-content-range-tags tags)) + attachments) + (tp--context-content-range-attachments context))) + (unless (gethash object (tp--context-retained context)) + (tp-object-retain context object))) + +(defun tp--attach-content-ranges (context leaf ranges copy-tags-p) + "Attach RANGES to LEAF for CONTEXT, copying TAGS when COPY-TAGS-P is non-nil." + (tp--validate-context-object context leaf) + (let ((attachments (gethash leaf + (tp--context-content-range-attachments context))) + (copies (and copy-tags-p (make-hash-table :test #'eq))) + added) + (dolist (range ranges) + (let ((object (plist-get range :object)) + (start (plist-get range :start)) + (end (plist-get range :end)) + (tags (plist-get range :tags))) + (tp--validate-context-object context object) + (tp--validate-content-range start end) + (push (list :object object :start start :end end + :tags (if copy-tags-p + (tp--copy-content-range-tags tags copies) + tags)) + added) + (unless (gethash object (tp--context-retained context)) + (tp-object-retain context object)))) + (puthash leaf (nconc added attachments) + (tp--context-content-range-attachments context))) + ranges) + +(defun tp-object-attach-content-ranges (context leaf ranges) + "Attach multiple OBJECT ranges to candidate content LEAF for CONTEXT. +RANGES contains plists with `:object', `:start', `:end', and optional `:tags'. +TP defensively copies every non-nil TAGS value. The ranges retain input order +after publication and reuse a snapshot only when the same TAGS object recurs +in one bulk attach." + (tp--attach-content-ranges context leaf ranges t)) + +(defun tp-object-attach-content-ranges-owned (context leaf ranges) + "Attach candidate RANGES to LEAF for CONTEXT with owned TAGS snapshots. +Use this only when each non-nil TAGS value was freshly allocated for the +candidate and will not be mutated or exposed to caller mutation after this +call. TP transfers those values without copying; failed candidates discard +the transferred values with the rest of their prepare state." + (tp--attach-content-ranges context leaf ranges nil)) + (defun tp--touch-context-binding (context binding) "Record BINDING as touched by CONTEXT." (let ((owner (tp-binding-owner binding))) @@ -384,13 +610,20 @@ marker-backed mounts after publication." (signal 'tp-cross-surface-object (list owner))) (puthash binding t (tp--context-bindings context)))) -(defun tp--plist-overlay (parent child) - "Return a fresh plist where CHILD values override PARENT values." - (let ((result (tp--copy-property-value parent))) +(defun tp--plist-overlay (parent child &optional owned-p) + "Return a fresh plist where CHILD values override PARENT values. +When OWNED-P is non-nil, reuse candidate-owned property values while copying +only the plist spine. The non-owned path keeps the defensive value copies +needed by ordinary TP callers." + (let ((result (if owned-p + (copy-sequence parent) + (tp--copy-property-value parent)))) (cl-loop for (property value) on child by #'cddr do (setq result (plist-put result property - (tp--copy-property-value value)))) + (if owned-p + value + (tp--copy-property-value value))))) result)) (defun tp--plan-segment (plan position) @@ -398,13 +631,14 @@ marker-backed mounts after publication." (or (tp-surface-plan-key plan) (list :position position :kind (tp-surface-plan-kind plan)))) -(defun tp--render-plan (plan context) - "Return PLAN's propertized string and side records in CONTEXT." +(defun tp--render-plan (plan context &optional owned-p) + "Return PLAN's propertized string and side records in CONTEXT. +OWNED-P transfers candidate-local property values through the render pass." (let (records) (cl-labels ((walk (node path inherited offset) (let* ((props (tp--plist-overlay - inherited (tp-surface-plan-props node))) + inherited (tp-surface-plan-props node) owned-p)) (begin offset) (children (tp-surface-plan-children node)) pieces) @@ -425,7 +659,8 @@ marker-backed mounts after publication." (unless object (signal 'tp-orphan-object (list path))) (push (list :path path :object object :start begin :end offset - :props props :tags (tp-surface-plan-tags node)) + :props props :tags (tp-surface-plan-tags node) + :leaf (null children)) records)) (cons (apply #'concat (nreverse pieces)) offset)))) (let* ((path (list (tp--plan-segment plan 0))) @@ -492,17 +727,34 @@ marker-backed mounts after publication." (dolist (child (tp-surface-plan-children plan)) (tp--validate-plan-capability child capability)))) -(defun tp--producer-result (value surface options) - "Normalize producer VALUE for SURFACE using OPTIONS." +(defun tp--producer-result (value surface options context) + "Normalize producer VALUE for SURFACE and active CONTEXT using OPTIONS." (cond ((tp-surface-result-p value) - (cons (tp--copy-surface-plan (tp-surface-result-plan value)) - (tp-surface-result-client-state value))) + (let ((owned-p (tp-surface-result-plan-owned-p value))) + (if owned-p + (progn + (unless (and context + (tp--context-active context) + (eq context tp--current-prepare-context) + (eq context (tp-surface-result-owner-context value)) + (not (tp-surface-result-consumed-p value))) + (signal 'tp-owned-result-error (list value context))) + (let ((plan (tp-surface-result-plan value)) + (client-state (tp-surface-result-client-state value))) + (setf (tp-surface-result-consumed-p value) t + (tp-surface-result-plan value) nil + (tp-surface-result-client-state value) nil + (tp-surface-result-owner-context value) nil) + (list plan client-state t))) + (list (tp--copy-surface-plan (tp-surface-result-plan value)) + (tp-surface-result-client-state value) nil)))) ((tp-surface-plan-p value) - (cons (tp--copy-surface-plan value) + (list (tp--copy-surface-plan value) (if (plist-member options :client-state) (plist-get options :client-state) - (tp--surface-client-state surface)))) + (tp--surface-client-state surface)) + nil)) (t (signal 'tp-invalid-surface-plan (list value))))) (defun tp--context-binding-removals (context objects) @@ -553,34 +805,71 @@ marker-backed mounts after publication." count (and old-position (/= old-position position)))))) (defun tp--candidate-object-list (context) - "Return CONTEXT's touched objects." + "Return CONTEXT's touched and retained candidate objects." (let (objects) - (maphash (lambda (object _present) (push object objects)) - (tp--context-touched context)) + (maphash + (lambda (_path object) + (when (or (gethash object (tp--context-touched context)) + (gethash object (tp--context-retained context))) + (push object objects))) + (tp--context-objects context)) objects)) -(defun tp--content-mount-spec (object record &optional tags) - "Return one content mount spec for OBJECT over RECORD with TAGS." +(defun tp--candidate-reconciled-count (context created) + "Return the number of touched retained objects in CONTEXT excluding CREATED." + (- (hash-table-count (tp--context-touched context)) + (length created))) + +(defun tp--content-mount-spec (object record &optional tags tags-owned-p) + "Return one content mount spec for OBJECT over RECORD with TAGS. +TAGS-OWNED-P skips a second snapshot when TAGS already belongs to the candidate." (list :object object :start (plist-get record :start) :end (plist-get record :end) - :tags (tp--copy-property-value - (if tags tags (plist-get record :tags))))) + :tags (if tags-owned-p + tags + (tp--copy-property-value + (if tags tags (plist-get record :tags)))))) (defun tp--content-mount-specs (records context) - "Return direct and logical-fragment mount specs for RECORDS in CONTEXT." + "Return direct and logical range mount specs for RECORDS in CONTEXT." (let (specs) (dolist (record records) - (let* ((fragment (plist-get record :object)) - (attachments + (let* ((leaf (plist-get record :object)) + (fragment-attachments (nreverse (copy-sequence - (gethash fragment - (tp--context-fragment-attachments context)))))) - (push (tp--content-mount-spec fragment record) specs) - (dolist (attachment attachments) + (gethash leaf + (tp--context-fragment-attachments context))))) + (range-attachments + (nreverse + (copy-sequence + (gethash leaf + (tp--context-content-range-attachments context)))))) + (push (tp--content-mount-spec leaf record) specs) + (dolist (attachment fragment-attachments) (push (tp--content-mount-spec - (car attachment) record (cdr attachment)) - specs)))) + (car attachment) record (cdr attachment) t) + specs)) + (when range-attachments + (unless (plist-get record :leaf) + (signal 'tp-invalid-content-range + (list :leaf leaf :reason 'not-a-text-leaf))) + (let ((leaf-start (plist-get record :start)) + (leaf-length (- (plist-get record :end) + (plist-get record :start)))) + (dolist (attachment range-attachments) + (let ((start (plist-get attachment :start)) + (end (plist-get attachment :end))) + (unless (and (<= end leaf-length) + (<= start end)) + (signal 'tp-invalid-content-range + (list :start start :end end + :leaf-length leaf-length))) + (push (list :object (plist-get attachment :object) + :start (+ leaf-start start) + :end (+ leaf-start end) + :tags (plist-get attachment :tags)) + specs))))))) (nreverse specs))) (defun tp--plan-record-mount-specs (records capability context) @@ -610,8 +899,8 @@ marker-backed mounts after publication." (dolist (other tp--buffer-surfaces) (unless (eq other surface) (dolist (mount (tp--surface-mounts other)) - (let ((start (marker-position (tp--surface-mount-start mount))) - (end (marker-position (tp--surface-mount-end mount)))) + (let ((start (tp--mount-position (tp--surface-mount-start mount))) + (end (tp--mount-position (tp--surface-mount-end mount)))) (when (cl-some (lambda (range) (tp--ranges-overlap-p (car range) (cdr range) start end)) @@ -626,10 +915,13 @@ marker-backed mounts after publication." (tp--binding-touch-function (lambda (binding) (tp--touch-context-binding context binding)))) (if (functionp input) - (tp--producer-result (funcall input context) surface options) + (tp--producer-result (funcall input context) surface options context) (let ((plan (tp--copy-surface-plan input))) (tp--ensure-plan-objects context plan) - (tp--producer-result plan surface options))))) + (list plan + (if (plist-member options :client-state) + (plist-get options :client-state) + (tp--surface-client-state surface))))))) (defun tp--call-with-prepare-buffer-guard (surface function) "Call FUNCTION while rejecting producer edits to SURFACE's buffer." @@ -677,6 +969,21 @@ marker-backed mounts after publication." (setcdr (car merged) (max (cdar merged) (cdr range))) (push range merged))))) +(defun tp--subtract-ranges (ranges exclusions) + "Return RANGES with EXCLUSIONS removed. +Both arguments contain numeric half-open relative intervals." + (let (result) + (dolist (range ranges (nreverse result)) + (let ((cursor (car range)) + (end (cdr range))) + (dolist (exclusion exclusions) + (when (and (< cursor end) (> (cdr exclusion) cursor)) + (when (< cursor (car exclusion)) + (push (cons cursor (min end (car exclusion))) result)) + (setq cursor (max cursor (cdr exclusion))))) + (when (< cursor end) + (push (cons cursor end) result)))))) + (defun tp--live-scope-ranges (surface objects &optional relative) "Return live mount ranges for OBJECTS on SURFACE. When RELATIVE is non-nil, return offsets from the surface start." @@ -686,8 +993,8 @@ When RELATIVE is non-nil, return offsets from the surface start." ranges) (dolist (object objects) (dolist (mount (gethash object (tp--surface-mount-index surface))) - (let ((start (marker-position (tp--surface-mount-start mount))) - (end (marker-position (tp--surface-mount-end mount)))) + (let ((start (tp--mount-position (tp--surface-mount-start mount))) + (end (tp--mount-position (tp--surface-mount-end mount)))) (unless (and start end) (signal 'tp-stale-mount (list object))) (push (cons (- start base) (- end base)) ranges)))) @@ -849,12 +1156,13 @@ OPTIONS configure the mount and INITIAL is non-nil for first publication." surface (lambda () (tp--prepare-input surface input options context)))) - (plan (car normalized)) - (client-state (cdr normalized)) + (plan (nth 0 normalized)) + (client-state (nth 1 normalized)) + (owned-p (nth 2 normalized)) (_capability (tp--validate-plan-capability plan (tp--surface-capability surface))) (_tree (tp--validate-context-tree context plan)) - (render-result (tp--render-plan plan context)) + (render-result (tp--render-plan plan context owned-p)) (rendered (car render-result)) (records (cdr render-result)) (objects (tp--candidate-object-list context)) @@ -885,7 +1193,7 @@ OPTIONS configure the mount and INITIAL is non-nil for first publication." :client-state client-state :producer input :initial initial :created created :removed removed :moved (tp--plan-moved-count (tp--surface-plan surface) plan) - :reconciled (- (length objects) (length created)) + :reconciled (tp--candidate-reconciled-count context created) :scope-objects scope-objects :scope-patches (plist-get scope-analysis :patches) :scope-fallback (plist-get scope-analysis :fallback))) @@ -1241,7 +1549,11 @@ MOUNT-SPECS and SURFACE provide ranges for PROPERTIES." (defun tp-surface-mount (buffer plan-or-producer &optional options) "Mount PLAN-OR-PRODUCER into BUFFER according to OPTIONS. OPTIONS accepts :capability (`content' or `properties'), :start, :end, -:inhibit-read-only, :client-state, and :observers. Return a surface handle." +:inhibit-read-only, :coordinate-mounts, :client-state, and :observers. When +:coordinate-mounts is non-nil, content mounts store numeric coordinates rather +than private markers; use it only when host edits make the whole content +surface stale and all writes are published through TP. Return a surface +handle." (let* ((target (get-buffer buffer)) (_live (unless (buffer-live-p target) (signal 'tp-unsupported-buffer (list buffer)))) @@ -1274,11 +1586,17 @@ PLAN-OR-PRODUCER has the same full candidate contract as `tp-surface-update'. TP resolves OBJECTS through its mount index and rejects any candidate output change outside their ranges before publication. -OPTIONS accepts `:on-mismatch'. Its default, `error', signals -`tp-scope-mismatch'; `root' permits an explicit full-surface fallback." +OPTIONS accepts `:on-mismatch' and `:return-report'. Its default mismatch +policy, `error', signals `tp-scope-mismatch'; `root' permits an explicit +full-surface fallback. The report is returned as a defensive snapshot by +default; `:return-report nil' avoids materializing that snapshot when the +caller reads the scalar summary from the surface instead." (tp--validate-live-surface surface) (setq objects (tp--normalize-surface-scopes surface objects)) - (let ((on-mismatch (or (plist-get options :on-mismatch) 'error))) + (let ((on-mismatch (or (plist-get options :on-mismatch) 'error)) + (return-report (if (plist-member options :return-report) + (plist-get options :return-report) + t))) (unless (memq on-mismatch '(error root)) (signal 'wrong-type-argument (list '(member error root) on-mismatch))) @@ -1287,8 +1605,9 @@ OPTIONS accepts `:on-mismatch'. Its default, `error', signals (tp--install-surface-producer surface plan-or-producer (tp--surface-options surface) nil) (tp--set-surface-scope-request - surface objects (list :on-mismatch on-mismatch))))) - (tp-surface-report surface)) + surface objects (list :on-mismatch on-mismatch)))) + (when return-report + (tp-surface-report surface)))) (defun tp--edit-touches-span-p (beg edit-end start end) "Return non-nil when BEG..EDIT-END touches the old START..END span." @@ -1433,8 +1752,8 @@ OPTIONS accepts `:on-mismatch'. Its default, `error', signals (mapcar (lambda (mount) (list (tp--surface-mount-object mount) (tp--surface-mount-anchor mount) - (- (marker-position (tp--surface-mount-start mount)) base) - (- (marker-position (tp--surface-mount-end mount)) base) + (- (tp--mount-position (tp--surface-mount-start mount)) base) + (- (tp--mount-position (tp--surface-mount-end mount)) base) (tp--surface-mount-tags mount))) (tp--surface-mounts surface)))) @@ -1500,6 +1819,23 @@ OPTIONS accepts `:on-mismatch'. Its default, `error', signals (setq count (1+ count))) count)) +(defun tp--validate-published-content-range (buffer start rendered) + "Validate inserted BUFFER text from START with RENDERED. +Return whether the properties match. +An external `after-change-functions' hook may alter the inserted properties; +return nil for that recoverable case so TP reapplies the rendered runs. A +character rewrite is not recoverable at the property layer and signals before +the transaction can commit." + (with-current-buffer buffer + (let ((end (+ start (length rendered)))) + (unless (and (<= (point-min) start) (<= end (point-max)) + (equal (buffer-substring-no-properties start end) + (substring-no-properties rendered))) + (signal 'tp-publication-mismatch + (list :buffer buffer :start start :end end))) + (equal-including-properties + (buffer-substring start end) rendered)))) + (defun tp--content-text-operation (surface rendered) "Publish SURFACE's minimal character replacement for RENDERED." (pcase-let* ((`(,start . ,end) (tp--surface-range surface)) @@ -1510,18 +1846,30 @@ OPTIONS accepts `:on-mismatch'. Its default, `error', signals (unless (equal old new) (delete-region (+ start prefix) (- end suffix)) (goto-char (+ start prefix)) - (insert (substring new prefix (- (length new) suffix)))) + (let ((inserted (substring rendered prefix (- (length rendered) suffix)))) + (insert inserted) + (when (tp--validate-published-content-range + (tp--surface-buffer surface) (+ start prefix) inserted) + (push (cons prefix (- (length new) suffix)) + tp--content-published-ranges)))) (set-marker (tp--surface-start surface) start) (set-marker (tp--surface-end surface) (+ start (length new))) (if (equal old new) 0 1))) (defun tp--string-property-run-diff-p (buffer start rendered from to) - "Return non-nil when BUFFER at START differs from RENDERED on FROM..TO." - (cl-loop for offset from from below to - thereis - (not (tp--plan-props-equal-p - (text-properties-at (+ start offset) buffer) - (text-properties-at offset rendered))))) + "Return non-nil when BUFFER at START differs from RENDERED on FROM..TO. +The rendered interval is already one direct-property run. Compare its +boundary value once and reject an interior buffer boundary; scanning every +character makes a large rendered surface quadratic in its number of runs." + (let* ((buffer-start (+ start from)) + (buffer-end (+ start to)) + (buffer-next (or (next-property-change + buffer-start buffer buffer-end) + buffer-end))) + (or (not (tp--plan-props-equal-p + (text-properties-at buffer-start buffer) + (text-properties-at from rendered))) + (< buffer-next buffer-end)))) (defun tp--content-property-operations-in-range (buffer start rendered from to) @@ -1545,6 +1893,11 @@ When SCOPED is non-nil, inspect only RANGES, including an empty set." (let* ((buffer (tp--surface-buffer surface)) (start (marker-position (tp--surface-start surface))) (ranges (if scoped ranges (list (cons 0 (length rendered))))) + (ranges + (if tp--content-published-ranges + (tp--subtract-ranges + ranges (tp--coalesce-ranges tp--content-published-ranges)) + ranges)) operations) (dolist (range ranges operations) (setq operations @@ -1580,18 +1933,24 @@ When SCOPED is non-nil, inspect only RANGES, including an empty set." (tp--apply-property-operation buffer operation)))))) (defun tp--make-content-mounts (prepared) - "Create marker-backed content mounts for PREPARED." + "Create content mounts for PREPARED using its declared coordinate policy." (let* ((surface (tp--prepared-surface-surface prepared)) (buffer (tp--surface-buffer surface)) - (base (marker-position (tp--surface-start surface)))) + (base (marker-position (tp--surface-start surface))) + (coordinate-p (plist-get (tp--surface-options surface) + :coordinate-mounts))) (mapcar (lambda (spec) (tp--make-surface-mount :object (plist-get spec :object) - :start (with-current-buffer buffer - (copy-marker (+ base (plist-get spec :start)) nil)) - :end (with-current-buffer buffer - (copy-marker (+ base (plist-get spec :end)) t)) + :start (if coordinate-p + (+ base (plist-get spec :start)) + (with-current-buffer buffer + (copy-marker (+ base (plist-get spec :start)) nil))) + :end (if coordinate-p + (+ base (plist-get spec :end)) + (with-current-buffer buffer + (copy-marker (+ base (plist-get spec :end)) t))) :tags (plist-get spec :tags) :capability 'content)) (tp--prepared-surface-mount-specs prepared)))) @@ -1718,11 +2077,22 @@ Return the number of text operations." (let* ((start (+ base (plist-get patch :old-start))) (end (+ base (plist-get patch :old-end))) (replacement (plist-get patch :replacement)) - (plain (substring-no-properties replacement))) - (unless (equal (buffer-substring-no-properties start end) plain) - (delete-region start end) - (goto-char start) - (insert plain) + (old-plain (buffer-substring-no-properties start end)) + (new-plain (substring-no-properties replacement)) + (prefix (tp--common-prefix-length old-plain new-plain)) + (suffix (tp--common-suffix-length old-plain new-plain prefix)) + (old-end (- end suffix)) + (new-end (- (length replacement) suffix))) + (unless (equal old-plain new-plain) + (delete-region (+ start prefix) old-end) + (goto-char (+ start prefix)) + (let ((inserted (substring replacement prefix new-end))) + (insert inserted) + (when (tp--validate-published-content-range + (tp--surface-buffer surface) (+ start prefix) inserted) + (push (cons (+ (plist-get patch :new-start) prefix) + (+ (plist-get patch :new-start) new-end)) + tp--content-published-ranges))) (cl-incf count)))) (set-marker (tp--surface-start surface) base) (set-marker (tp--surface-end surface) (+ base (length rendered))) @@ -1738,7 +2108,8 @@ Return the number of text operations." (patches (tp--prepared-surface-scope-patches prepared)) (text-operations 0) property-operations) - (with-current-buffer buffer + (let (tp--content-published-ranges) + (with-current-buffer buffer (save-restriction (widen) (let ((inhibit-read-only @@ -1757,7 +2128,7 @@ Return the number of text operations." (and scoped (not fallback)))) (tp--apply-content-property-operations buffer property-operations) (tp--publication-step 'property surface)))) - (cons text-operations (length property-operations)))) + (cons text-operations (length property-operations))))) (defun tp--publish-buffer-properties (prepared) "Publish PREPARED's host-range property operations." @@ -1977,10 +2348,10 @@ Return the number of text operations." (tp--surface-snapshot-mounts snapshot) (tp--surface-index surface) (tp--surface-snapshot-index snapshot) - (tp--surface-mount-index surface) - (tp--surface-snapshot-mount-index snapshot) - (tp--surface-ledger surface) - (tp--surface-snapshot-ledger snapshot))) + (tp--surface-mount-index surface) + (tp--surface-snapshot-mount-index snapshot) + (tp--surface-ledger surface) + (tp--surface-snapshot-ledger snapshot))) (when (and (tp--surface-live surface) (not (tp--surface-snapshot-live snapshot))) (tp--unregister-surface surface)) @@ -1993,7 +2364,7 @@ Return the number of text operations." (tp--surface-snapshot-live snapshot) (tp--surface-object-disposed object) nil)) (tp--surface-snapshot-objects snapshot))) - (setf (tp--surface-plan surface) (tp--surface-snapshot-plan snapshot) + (setf (tp--surface-plan surface) (tp--surface-snapshot-plan snapshot) (tp--surface-objects surface) (tp--surface-snapshot-objects snapshot) (tp--surface-mounts surface) (tp--surface-snapshot-mounts snapshot) (tp--surface-index surface) (tp--surface-snapshot-index snapshot) @@ -2052,7 +2423,9 @@ Return the number of text operations." (defun tp--cancel-change-group-safely (group) "Cancel active change GROUP without allowing quit to interrupt rollback." (when group - (let ((inhibit-quit t)) + (let ((inhibit-quit t) + (inhibit-modification-hooks t) + (tp--surface-publishing t)) (condition-case nil (cancel-change-group group) (error nil))))) @@ -2148,10 +2521,11 @@ Return the number of text operations." (defun tp--enqueue-surface-observers (surface) "Schedule SURFACE observers outside the publishing transaction." - (let ((observers (copy-sequence (tp--surface-observers surface))) - (report (tp-surface-report surface))) - (tp--enqueue-after-commit - (lambda () (tp--run-surface-observers surface observers report))))) + (when-let ((observers (tp--surface-observers surface))) + (let ((observers (copy-sequence observers)) + (report (tp-surface-report surface))) + (tp--enqueue-after-commit + (lambda () (tp--run-surface-observers surface observers report)))))) (defun tp--surface-commit-transaction () "Commit buffer edits and finalize every transaction surface." @@ -2189,10 +2563,11 @@ Return the number of text operations." (unwind-protect (let* ((normalized (tp--prepare-input surface plan-or-producer nil context)) - (plan (car normalized))) + (plan (car normalized)) + (owned-p (nth 2 normalized))) (tp--validate-plan-capability plan 'content) (tp--validate-context-tree context plan) - (setq rendered (car (tp--render-plan plan context)) + (setq rendered (car (tp--render-plan plan context owned-p)) success t) rendered) (setf (tp--context-active context) t) @@ -2387,8 +2762,8 @@ owns the property; conflicting host values are preserved and reported." (let ((point (or position (point)))) (dolist (surface tp--buffer-surfaces) (dolist (mount (tp--surface-index surface)) - (let ((start (marker-position (tp--surface-mount-start mount))) - (end (marker-position (tp--surface-mount-end mount)))) + (let ((start (tp--mount-position (tp--surface-mount-start mount))) + (end (tp--mount-position (tp--surface-mount-end mount)))) (when (and start end (<= start point) (< point end)) (cl-pushnew (tp--surface-mount-object mount) objects :test #'eq))))))) @@ -2403,6 +2778,26 @@ owns the property; conflicting host values are preserved and reported." (signal 'wrong-type-argument (list 'tp-surface-p surface))) (tp--copy-property-value (tp--surface-report surface))) +(defun tp-surface-report-summary (surface) + "Return scalar commit metrics for SURFACE without copying its full report. +The returned plist contains only numbers, symbols, booleans, or nil. Use +`tp-surface-report' when callers need arbitrary report payloads or a defensive +snapshot of nested values." + (unless (tp-surface-p surface) + (signal 'wrong-type-argument (list 'tp-surface-p surface))) + (let ((report (tp--surface-report surface))) + (list :transaction-id (plist-get report :transaction-id) + :text-operations (plist-get report :text-operations) + :property-operations (plist-get report :property-operations) + :full-root (plist-get report :full-root) + :scope-count (plist-get report :scope-count) + :scope-range-count (plist-get report :scope-range-count) + :scope-fallback (plist-get report :scope-fallback) + :reconciled-objects (plist-get report :reconciled-objects) + :created-objects (plist-get report :created-objects) + :removed-objects (plist-get report :removed-objects) + :moved-objects (plist-get report :moved-objects)))) + (defun tp-surface-inspect (surface) "Return read-only retained diagnostics for SURFACE." (tp--validate-live-surface surface)