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:
parent
84ebf0a548
commit
905d3523ac
@ -6,6 +6,7 @@ All notable changes to the tp library are documented here.
|
||||
|
||||
### 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 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.
|
||||
|
||||
10
README.md
10
README.md
@ -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.
|
||||
|
||||
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
|
||||
|
||||
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-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
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
|
||||
10
README_CN.md
10
README_CN.md
@ -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 会被拒绝,避免所有权静默分裂。
|
||||
|
||||
普通调用者不必手工构造 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 参数规范**:所有函数支持多种灵活的调用方式,同时适用于字符串和缓冲区
|
||||
@ -539,6 +541,14 @@ tp.el 所有函数按类别组织的完整概览:
|
||||
| `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 |
|
||||
|
||||
#### 便利 API
|
||||
|
||||
| 函数 | 描述 |
|
||||
|------|------|
|
||||
| `tp-propertize` | 通过 schema/cascade/projector core 返回带样式的字符串副本 |
|
||||
| `tp-apply` | 把原生 declarations 一次性应用到已有 buffer range |
|
||||
| `tp-watch` | 通过 properties surface 响应式维护已有 range 上的 declarations |
|
||||
|
||||
#### 属性层定义函数
|
||||
| 函数 | 描述 |
|
||||
|------|------|
|
||||
|
||||
@ -202,3 +202,9 @@ tp 不会把外部属性收编成新的匿名层,也不会静默覆盖它。
|
||||
- 同一 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 rollback,TP 的精确 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 会开启新 transaction;observer 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。
|
||||
|
||||
@ -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。
|
||||
|
||||
`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。
|
||||
|
||||
#### 依赖注册与管理
|
||||
| 函数/变量 | 描述 |
|
||||
|------|------|
|
||||
|
||||
95
tests/tp-convenience-tests.el
Normal file
95
tests/tp-convenience-tests.el
Normal 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
|
||||
21
tp-core.el
21
tp-core.el
@ -30,6 +30,8 @@
|
||||
:prefix "tp-"
|
||||
:group 'development)
|
||||
|
||||
(define-error 'tp-unsupported-buffer "Unsupported TP buffer")
|
||||
|
||||
(defcustom tp-debug-mode nil
|
||||
"When non-nil, enable debug logging for reactive updates.
|
||||
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))
|
||||
(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)
|
||||
"Return canonical presence of PROPERTY at POS in OBJECT."
|
||||
(let ((cell (plist-member (text-properties-at pos object) property)))
|
||||
|
||||
26
tp-ops.el
26
tp-ops.el
@ -24,6 +24,7 @@
|
||||
(require 'cl-lib)
|
||||
(require 'dash)
|
||||
(require 'tp-core)
|
||||
(require 'tp-style)
|
||||
(require 'tp-reactive)
|
||||
(require 'tp-layer)
|
||||
|
||||
@ -437,6 +438,31 @@ Returns a new propertized string."
|
||||
do (put-text-property start end key val 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)
|
||||
"Set text properties on string or buffer region.
|
||||
|
||||
|
||||
@ -912,6 +912,14 @@ or values plist. When PROVENANCE is non-nil, winner metadata is retained."
|
||||
(setq result (tp--deep-merge-plist result projected)))))
|
||||
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)
|
||||
|
||||
(provide 'tp-style)
|
||||
|
||||
@ -42,8 +42,6 @@
|
||||
(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-unsupported-buffer "Unsupported TP surface buffer"
|
||||
'tp-surface-error)
|
||||
|
||||
(cl-defstruct (tp-surface-plan (:constructor tp--make-surface-plan))
|
||||
"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)
|
||||
(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
|
||||
(buffer start end &key (start-insertion-type nil) (end-insertion-type t)
|
||||
(boundary-policy 'stale))
|
||||
@ -1927,6 +1914,36 @@ owns the property; conflicting host values are preserved and reported."
|
||||
:client-state (tp--surface-client-state 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--transaction-publish-functions #'tp--surface-publish-transaction)
|
||||
(add-hook 'tp--transaction-rollback-functions #'tp--surface-rollback-transaction)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user