feat(tp): add unified convenience APIs

Expose one-shot string and buffer styling plus reactive range watches over the same schema, mutation, binding, and retained surface core.

Verification: 712 ERT tests; 92 doctests; shuffled seed 20260806; compile-all WERROR=t.
This commit is contained in:
Kinneyzhang 2026-08-06 03:31:50 +08:00
parent 84ebf0a548
commit 905d3523ac
10 changed files with 209 additions and 13 deletions

View File

@ -6,6 +6,7 @@ All notable changes to the tp library are documented here.
### Added ### Added
- `tp-propertize`, `tp-apply`, and `tp-watch` now provide simple one-shot string, one-shot buffer-range, and reactive existing-text entry points over the same schema/cascade/projector and retained properties-surface core.
- TP 1.0 retained surfaces now provide defensive pure plans, prepare-scoped object identity, keyed/positional reconciliation, `content` and `properties` capabilities, marker-backed range anchors, same-surface overlapping property contributions, compare-before-write conflicts and explicit rebase, common-prefix/suffix text edits, property-run diffs, side indexes, opaque client state, generic reports, lifecycle cleanup, and atomic multi-buffer publication with exact rollback. Pure materialization uses the same plan semantics without leaving live handles or subscriptions. - TP 1.0 retained surfaces now provide defensive pure plans, prepare-scoped object identity, keyed/positional reconciliation, `content` and `properties` capabilities, marker-backed range anchors, same-surface overlapping property contributions, compare-before-write conflicts and explicit rebase, common-prefix/suffix text edits, property-run diffs, side indexes, opaque client state, generic reports, lifecycle cleanup, and atomic multi-buffer publication with exact rollback. Pure materialization uses the same plan semantics without leaving live handles or subscriptions.
- TP 1.0 signals and bindings now form an exact source→binding and binding→binding dependency graph with conditional rewiring, memoized equality cutoffs, transaction-local candidate signal values, deduplicated topological flushing, nested-write stabilization, rollback, cycle paths, owner disposal, buffer-scoped sources, variable adapters, and public scheduler counters. The legacy layer scanner remains isolated only until the retained-surface cutover. - TP 1.0 signals and bindings now form an exact source→binding and binding→binding dependency graph with conditional rewiring, memoized equality cutoffs, transaction-local candidate signal values, deduplicated topological flushing, nested-write stabilization, rollback, cycle paths, owner disposal, buffer-scoped sources, variable adapters, and public scheduler counters. The legacy layer scanner remains isolated only until the retained-surface cutover.
- The first TP 1.0 runtime slice: `tp-style.el` provides atomic namespaced property schemas, structured subject selectors and combinators, deterministic origin/importance/layer/specificity/scope/source-order cascade, property-specific inheritance, tagged CSS-wide values, custom-property fallback/cycle handling, explicit `tp-computed` value sources, named declarations, provenance, and final Emacs-property projection. Ordinary function values remain literal. - The first TP 1.0 runtime slice: `tp-style.el` provides atomic namespaced property schemas, structured subject selectors and combinators, deterministic origin/importance/layer/specificity/scope/source-order cascade, property-specific inheritance, tagged CSS-wide values, custom-property fallback/cycle handling, explicit `tp-computed` value sources, named declarations, provenance, and final Emacs-property projection. Ordinary function values remain literal.

View File

@ -180,6 +180,8 @@ The TP 1.0 migration has begun with a pure schema-driven cascade kernel. `tp-sty
`tp-surface.el` completes the retained publication path from object identity to marker-backed mounts and buffers. A producer receives a short-lived prepare context, ensures candidate objects before computing output, and returns a defensive pure plan plus optional opaque client state. TP validates the whole candidate before publication, applies common-prefix/suffix text edits and property-run diffs, swaps plans/indexes/client state at one revision, and rolls every affected buffer and signal back together on failure. `content` owns a disjoint text span; `properties` decorates attached host ranges, detects external property conflicts, and requires explicit rebase. Overlap is composable inside one surface and rejected across independent surfaces so ownership cannot silently split. `tp-surface.el` completes the retained publication path from object identity to marker-backed mounts and buffers. A producer receives a short-lived prepare context, ensures candidate objects before computing output, and returns a defensive pure plan plus optional opaque client state. TP validates the whole candidate before publication, applies common-prefix/suffix text edits and property-run diffs, swaps plans/indexes/client state at one revision, and rolls every affected buffer and signal back together on failure. `content` owns a disjoint text span; `properties` decorates attached host ranges, detects external property conflicts, and requires explicit rebase. Overlap is composable inside one surface and rejected across independent surfaces so ownership cannot silently split.
Ordinary callers can use the same core without manually constructing runtime objects: `tp-propertize` returns a styled string copy, `tp-apply` applies declarations once to an existing buffer range, and `tp-watch` keeps an existing range's properties synchronized with signals. These calls accept native Emacs property declarations; callbacks such as `help-echo` remain literal values, while only explicitly wrapped `tp-computed` sources are evaluated by the cascade.
### Core Innovations ### Core Innovations
1. **Unified API Parameter Conventions**: All functions support multiple flexible calling patterns, working seamlessly with both strings and buffers 1. **Unified API Parameter Conventions**: All functions support multiple flexible calling patterns, working seamlessly with both strings and buffers
@ -544,6 +546,14 @@ A complete overview of all tp.el functions organized by category:
| `tp-surface-mount` / `tp-surface-update` / `tp-surface-unmount` | Own the only live publication lifecycle | | `tp-surface-mount` / `tp-surface-update` / `tp-surface-unmount` | Own the only live publication lifecycle |
| `tp-surface-at-point` / `tp-surface-inspect` / `tp-surface-report` | Query retained side indexes and generic commit diagnostics without scanning text | | `tp-surface-at-point` / `tp-surface-inspect` / `tp-surface-report` | Query retained side indexes and generic commit diagnostics without scanning text |
#### Convenience APIs
| Function | Description |
|----------|-------------|
| `tp-propertize` | Return a styled string copy using the schema/cascade/projector core |
| `tp-apply` | Apply native declarations once to an existing buffer range |
| `tp-watch` | Reactively maintain declarations on an existing range through a properties surface |
#### Property Layer Definition Functions #### Property Layer Definition Functions
| Function | Description | | Function | Description |
|----------|-------------| |----------|-------------|

View File

@ -179,6 +179,8 @@ TP 1.0 迁移已经从纯 schema-driven cascade kernel 开始。`tp-style.el`
`tp-surface.el` 补全了从 object identity 到 marker-backed mount 和 buffer 的 retained publication 路径。producer 获得短生命周期 prepare context在计算输出前确保 candidate object并返回防御性 pure plan 与可选 opaque client state。TP 在 publication 前验证整个 candidate执行 common-prefix/suffix text edit 与 property-run diff并以同一个 revision 切换 plan/index/client state失败时把所有受影响 buffer 与 signal 一起回滚。`content` 拥有一个不重叠的文本 span`properties` 装饰 attached host range检测外部 property conflict并要求显式 rebase。同一 surface 内的 overlap 可以合成;独立 surface 之间的 overlap 会被拒绝,避免所有权静默分裂。 `tp-surface.el` 补全了从 object identity 到 marker-backed mount 和 buffer 的 retained publication 路径。producer 获得短生命周期 prepare context在计算输出前确保 candidate object并返回防御性 pure plan 与可选 opaque client state。TP 在 publication 前验证整个 candidate执行 common-prefix/suffix text edit 与 property-run diff并以同一个 revision 切换 plan/index/client state失败时把所有受影响 buffer 与 signal 一起回滚。`content` 拥有一个不重叠的文本 span`properties` 装饰 attached host range检测外部 property conflict并要求显式 rebase。同一 surface 内的 overlap 可以合成;独立 surface 之间的 overlap 会被拒绝,避免所有权静默分裂。
普通调用者不必手工构造 runtime object也能使用同一个 core`tp-propertize` 返回带样式的字符串副本,`tp-apply` 把声明一次性应用到已有 buffer range`tp-watch` 则让已有 range 的属性随 signal 保持同步。这些入口接受 Emacs 原生 property declarations`help-echo` 等 callback 保持 literal value只有显式包装的 `tp-computed` source 才由 cascade 求值。
### 核心创新 ### 核心创新
1. **统一的 API 参数规范**:所有函数支持多种灵活的调用方式,同时适用于字符串和缓冲区 1. **统一的 API 参数规范**:所有函数支持多种灵活的调用方式,同时适用于字符串和缓冲区
@ -539,6 +541,14 @@ tp.el 所有函数按类别组织的完整概览:
| `tp-surface-mount` / `tp-surface-update` / `tp-surface-unmount` | 唯一 live publication lifecycle | | `tp-surface-mount` / `tp-surface-update` / `tp-surface-unmount` | 唯一 live publication lifecycle |
| `tp-surface-at-point` / `tp-surface-inspect` / `tp-surface-report` | 不扫描文本地查询 retained side index 与 generic commit diagnostics | | `tp-surface-at-point` / `tp-surface-inspect` / `tp-surface-report` | 不扫描文本地查询 retained side index 与 generic commit diagnostics |
#### 便利 API
| 函数 | 描述 |
|------|------|
| `tp-propertize` | 通过 schema/cascade/projector core 返回带样式的字符串副本 |
| `tp-apply` | 把原生 declarations 一次性应用到已有 buffer range |
| `tp-watch` | 通过 properties surface 响应式维护已有 range 上的 declarations |
#### 属性层定义函数 #### 属性层定义函数
| 函数 | 描述 | | 函数 | 描述 |
|------|------| |------|------|

View File

@ -202,3 +202,9 @@ tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。
- 同一 surface 的 overlapping properties contributions 按 plan order 和 native property schema merge独立 surfaces 当前不得重叠字符 ownership。这个限制把 journal owner 保持为唯一 surface避免两套 baseline 静默覆盖。 - 同一 surface 的 overlapping properties contributions 按 plan order 和 native property schema merge独立 surfaces 当前不得重叠字符 ownership。这个限制把 journal owner 保持为唯一 surface避免两套 baseline 静默覆盖。
- 最外层 transaction 先完成所有 producer/plan/conflict validation再按 surface id publish。multi-buffer change group 负责 text rollbackTP 的精确 property journal 覆盖 `with-silent-modifications`;失败恢复 buffer direct properties、markers/index、objects、plan、client state、revision、bindings 与 signal values。publish 中 killed buffer 不复活,其他 surfaces 与 sources 回滚。 - 最外层 transaction 先完成所有 producer/plan/conflict validation再按 surface id publish。multi-buffer change group 负责 text rollbackTP 的精确 property journal 覆盖 `with-silent-modifications`;失败恢复 buffer direct properties、markers/index、objects、plan、client state、revision、bindings 与 signal values。publish 中 killed buffer 不复活,其他 surfaces 与 sources 回滚。
- `tp-surface-report` 的字段只使用 transaction/surface/source/binding/object/text/property/conflict/observer/timing 通用词汇。observer 在成功 commit 且 publishing transaction 的动态范围退出后执行,因此 observer 中的 signal write 会开启新 transactionobserver error 只写入 report不回滚。 - `tp-surface-report` 的字段只使用 transaction/surface/source/binding/object/text/property/conflict/observer/timing 通用词汇。observer 在成功 commit 且 publishing transaction 的动态范围退出后执行,因此 observer 中的 signal write 会开启新 transactionobserver error 只写入 report不回滚。
## 15. 简单与响应式便利入口
- `tp-propertize STRING DECLARATIONS` 接受 Emacs 原生 property plist把它转换为 canonical `text/` declarations经 schema/cascade/projector 后应用到 STRING 的防御性副本;它不建立 object、binding、anchor 或 surface。
- `tp-apply BUFFER START END DECLARATIONS` 使用同一 projection 与现有 range mutation primitive只修改声明过的 direct properties保留文本和未声明 property并返回 `(START . END)`
- `tp-watch BUFFER START END COMPUTE` 创建一个 properties-only surface。COMPUTE 是返回原生 property plist 的零参数函数;其 signal/binding dependencies 由 exact graph 收集。返回值就是可传给 `tp-surface-inspect`、`tp-surface-report` 和 `tp-surface-unmount` 的 opaque surface。首次 publication 失败会释放 convenience 层创建的 anchor。

View File

@ -187,6 +187,8 @@ Stage 2 canonical façade 是内部模型,不改变公开入口和历史返回
`content` mount 可以替换其拥有的 disjoint span外部字符编辑会使 mount stale。`properties` mount 只能修改 attached anchor 上声明的 direct properties同一 surface 内重叠 contribution 通过 native property schema 合成。外部值与 TP last-published value 不同时prepare 报 `tp-property-conflict`,调用者必须 `tp-range-rebase` 或 unmount。独立 surfaces 的字符范围当前必须不重叠,以保持单一、可证明的 ownership journal。 `content` mount 可以替换其拥有的 disjoint span外部字符编辑会使 mount stale。`properties` mount 只能修改 attached anchor 上声明的 direct properties同一 surface 内重叠 contribution 通过 native property schema 合成。外部值与 TP last-published value 不同时prepare 报 `tp-property-conflict`,调用者必须 `tp-range-rebase` 或 unmount。独立 surfaces 的字符范围当前必须不重叠,以保持单一、可证明的 ownership journal。
`tp-propertize``tp-apply``tp-style` projection 加 `tp-ops` mutation primitive 的 one-shot 组合,不创建 runtime state。`tp-watch` 只把 range anchor、object binding 和 properties surface 组合成普通用户入口;它没有独立 scheduler、diff 或 publication path。
#### 依赖注册与管理 #### 依赖注册与管理
| 函数/变量 | 描述 | | 函数/变量 | 描述 |
|------|------| |------|------|

View File

@ -0,0 +1,95 @@
;;; tp-convenience-tests.el --- Tests for TP convenience APIs -*- lexical-binding: t; -*-
;; Copyright (C) 2026 Geekinney
;;; Commentary:
;; Contract tests for one-shot and reactive text-property conveniences.
;;; Code:
(require 'ert)
(require 'tp)
(ert-deftest tp-convenience-test-propertize-uses-style-projection ()
"Propertizing projects native declarations without mutating the input."
(let* ((callback (lambda (_window _object _position) "help"))
(source "Hello")
(result
(tp-propertize
source
(list 'face '(:foreground "red" :weight bold)
'help-echo callback 'keymap nil))))
(should (equal source "Hello"))
(should-not (text-properties-at 0 source))
(should (equal (get-text-property 0 'face result)
'(:foreground "red" :weight bold)))
(should (eq (get-text-property 0 'help-echo result) callback))
(should (plist-member (text-properties-at 0 result) 'keymap))
(should-not (get-text-property 0 'keymap result))))
(ert-deftest tp-convenience-test-apply-mutates-only-the-requested-range ()
"Applying declarations preserves text and properties outside the range."
(with-temp-buffer
(insert "abcdef")
(put-text-property 1 7 'category 'host)
(should (equal (tp-apply (current-buffer) 2 5 '(face italic)) '(2 . 5)))
(should (equal (buffer-string) "abcdef"))
(should-not (get-text-property 1 'face))
(should (eq (get-text-property 2 'face) 'italic))
(should-not (get-text-property 5 'face))
(should (eq (get-text-property 3 'category) 'host))))
(ert-deftest tp-convenience-test-apply-rejects-invalid-targets-and-ranges ()
"Applying never falls back from an invalid target to the current buffer."
(with-temp-buffer
(insert "safe")
(should-error (tp-apply " *missing-tp-buffer*" 1 2 '(face bold))
:type 'tp-unsupported-buffer)
(should-error (tp-apply (current-buffer) 4 2 '(face bold))
:type 'args-out-of-range)
(should (equal (buffer-string) "safe"))
(should-not (text-properties-at 0 (buffer-string)))))
(ert-deftest tp-convenience-test-watch-reacts-and-restores-host-properties ()
"Watching a range updates properties through one retained surface."
(with-temp-buffer
(insert "Status")
(put-text-property 1 7 'help-echo "host")
(let* ((connected (tp-signal-create nil))
(surface
(tp-watch
(current-buffer) 1 7
(lambda ()
(if (tp-signal-read connected)
'(face (:foreground "green") help-echo "Connected")
'(face (:foreground "red") help-echo "Disconnected"))))))
(should (equal (buffer-string) "Status"))
(should (equal (get-text-property 2 'face)
'(:foreground "red")))
(should (equal (get-text-property 2 'help-echo) "Disconnected"))
(tp-signal-set connected t)
(should (equal (get-text-property 2 'face)
'(:foreground "green")))
(should (equal (get-text-property 2 'help-echo) "Connected"))
(should (= (tp-surface-revision surface) 2))
(tp-surface-unmount surface)
(should-not (get-text-property 2 'face))
(should (equal (get-text-property 2 'help-echo) "host")))))
(ert-deftest tp-convenience-test-failed-watch-releases-its-range-anchor ()
"A failed first watch publication leaves no live marker-backed anchor."
(with-temp-buffer
(insert "host")
(let ((original (symbol-function 'tp-range-anchor-create))
anchor)
(cl-letf (((symbol-function 'tp-range-anchor-create)
(lambda (&rest arguments)
(setq anchor (apply original arguments)))))
(should-error
(tp-watch (current-buffer) 1 5
(lambda () (error "Initial compute failed")))))
(should-not (tp-range-anchor-live-p anchor)))))
(provide 'tp-convenience-tests)
;;; tp-convenience-tests.el ends here

View File

@ -30,6 +30,8 @@
:prefix "tp-" :prefix "tp-"
:group 'development) :group 'development)
(define-error 'tp-unsupported-buffer "Unsupported TP buffer")
(defcustom tp-debug-mode nil (defcustom tp-debug-mode nil
"When non-nil, enable debug logging for reactive updates. "When non-nil, enable debug logging for reactive updates.
Debug messages are logged to the *tp-debug* buffer and optionally Debug messages are logged to the *tp-debug* buffer and optionally
@ -161,6 +163,25 @@ FORMAT-STRING and ARGS are passed to `format'."
(max (or start (car bounds)) (car bounds)) (max (or start (car bounds)) (car bounds))
(min (or end (cdr bounds)) (cdr bounds))))) (min (or end (cdr bounds)) (cdr bounds)))))
(defun tp--buffer-position-p (position buffer)
"Return non-nil when POSITION belongs to BUFFER or is an integer."
(or (integerp position)
(and (markerp position)
(eq (marker-buffer position) buffer)
(marker-position position))))
(defun tp--validate-buffer-range (buffer start end)
"Validate START and END as an ordered live range in BUFFER."
(unless (buffer-live-p buffer)
(signal 'tp-unsupported-buffer (list buffer)))
(with-current-buffer buffer
(save-restriction
(widen)
(unless (and (tp--buffer-position-p start buffer)
(tp--buffer-position-p end buffer)
(<= (point-min) start) (<= start end) (<= end (point-max)))
(signal 'args-out-of-range (list buffer start end))))))
(defun tp--presence-at (pos property &optional object) (defun tp--presence-at (pos property &optional object)
"Return canonical presence of PROPERTY at POS in OBJECT." "Return canonical presence of PROPERTY at POS in OBJECT."
(let ((cell (plist-member (text-properties-at pos object) property))) (let ((cell (plist-member (text-properties-at pos object) property)))

View File

@ -24,6 +24,7 @@
(require 'cl-lib) (require 'cl-lib)
(require 'dash) (require 'dash)
(require 'tp-core) (require 'tp-core)
(require 'tp-style)
(require 'tp-reactive) (require 'tp-reactive)
(require 'tp-layer) (require 'tp-layer)
@ -437,6 +438,31 @@ Returns a new propertized string."
do (put-text-property start end key val result)) do (put-text-property start end key val result))
result)))))) result))))))
;;;###autoload
(defun tp-propertize (string declarations)
"Return a copy of STRING styled by native DECLARATIONS.
DECLARATIONS pass through TP's property schemas, cascade, and projector.
Ordinary function values remain literal text-property values."
(unless (stringp string)
(signal 'wrong-type-argument (list 'stringp string)))
(tp--apply-props-to-string
string 0 (length string)
(tp--project-text-declarations declarations)))
;;;###autoload
(defun tp-apply (buffer start end declarations)
"Apply native DECLARATIONS once to BUFFER from START to END.
The operation preserves text and direct properties not named by DECLARATIONS.
Return the committed range as a START . END cons."
(let* ((target (get-buffer buffer))
(_range (tp--validate-buffer-range target start end))
(range (tp--native-range-from-object target start end))
(properties (tp--project-text-declarations declarations)))
(tp--apply-props-by-operation
(tp--native-range-start range) (tp--native-range-end range)
properties target nil)
(cons (tp--native-range-start range) (tp--native-range-end range))))
(defun tp-set (start-or-string &optional end-or-prop props-or-val &rest rest) (defun tp-set (start-or-string &optional end-or-prop props-or-val &rest rest)
"Set text properties on string or buffer region. "Set text properties on string or buffer region.

View File

@ -912,6 +912,14 @@ or values plist. When PROVENANCE is non-nil, winner metadata is retained."
(setq result (tp--deep-merge-plist result projected))))) (setq result (tp--deep-merge-plist result projected)))))
result)) result))
(defun tp--project-text-declarations (declarations)
"Project native text property DECLARATIONS through the style core."
(tp-project-style
(tp-compute-style
(tp-subject-create :type 'text)
:declarations (tp-text-declarations declarations)
:rules nil)))
(tp--register-default-text-properties) (tp--register-default-text-properties)
(provide 'tp-style) (provide 'tp-style)

View File

@ -42,8 +42,6 @@
(define-error 'tp-dead-surface "Dead TP surface" 'tp-surface-error) (define-error 'tp-dead-surface "Dead TP surface" 'tp-surface-error)
(define-error 'tp-invalid-range-anchor "Invalid TP range anchor" (define-error 'tp-invalid-range-anchor "Invalid TP range anchor"
'tp-surface-error) 'tp-surface-error)
(define-error 'tp-unsupported-buffer "Unsupported TP surface buffer"
'tp-surface-error)
(cl-defstruct (tp-surface-plan (:constructor tp--make-surface-plan)) (cl-defstruct (tp-surface-plan (:constructor tp--make-surface-plan))
"Pure retained node data accepted by a TP surface." "Pure retained node data accepted by a TP surface."
@ -579,17 +577,6 @@ OPTIONS configure the mount and INITIAL is non-nil for first publication."
result) result)
(unless success (tp--discard-context context))))) (unless success (tp--discard-context context)))))
(defun tp--validate-buffer-range (buffer start end)
"Validate START and END as an ordered live range in BUFFER."
(unless (buffer-live-p buffer)
(signal 'tp-unsupported-buffer (list buffer)))
(with-current-buffer buffer
(save-restriction
(widen)
(unless (and (integer-or-marker-p start) (integer-or-marker-p end)
(<= (point-min) start) (<= start end) (<= end (point-max)))
(signal 'args-out-of-range (list buffer start end))))))
(cl-defun tp-range-anchor-create (cl-defun tp-range-anchor-create
(buffer start end &key (start-insertion-type nil) (end-insertion-type t) (buffer start end &key (start-insertion-type nil) (end-insertion-type t)
(boundary-policy 'stale)) (boundary-policy 'stale))
@ -1927,6 +1914,36 @@ owns the property; conflicting host values are preserved and reported."
:client-state (tp--surface-client-state surface) :client-state (tp--surface-client-state surface)
:report (tp-surface-report surface))) :report (tp-surface-report surface)))
(defun tp--watch-producer (anchor compute)
"Return a properties producer backed by ANCHOR and COMPUTE."
(lambda (context)
(let* ((object (tp-object-ensure
context nil 'watch 'text-properties))
(binding
(tp-bind object '(tp/watch . declarations)
(lambda ()
(tp--project-text-declarations (funcall compute))))))
(tp-object-attach-range context object anchor)
(tp-surface-plan-create
:key 'watch :kind 'text-properties
:props (tp-binding-read binding) :capability 'properties))))
;;;###autoload
(defun tp-watch (buffer start end compute)
"Reactively apply declarations from COMPUTE to BUFFER from START to END.
COMPUTE is a zero-argument function returning native text declarations.
Return the underlying properties-only surface for inspection or unmounting."
(unless (functionp compute)
(signal 'wrong-type-argument (list 'functionp compute)))
(let ((anchor (tp-range-anchor-create buffer start end)) surface)
(unwind-protect
(setq surface
(tp-surface-mount
buffer (tp--watch-producer anchor compute)
'(:capability properties :inhibit-read-only t)))
(unless surface (tp--dispose-anchor anchor)))
surface))
(add-hook 'tp--binding-changed-functions #'tp--surface-binding-changed) (add-hook 'tp--binding-changed-functions #'tp--surface-binding-changed)
(add-hook 'tp--transaction-publish-functions #'tp--surface-publish-transaction) (add-hook 'tp--transaction-publish-functions #'tp--surface-publish-transaction)
(add-hook 'tp--transaction-rollback-functions #'tp--surface-rollback-transaction) (add-hook 'tp--transaction-rollback-functions #'tp--surface-rollback-transaction)