243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
# TP
|
|
|
|
TP 2.0 is a standalone retained/reactive text runtime for Emacs. It projects declarative properties, reactive data, and stable text objects onto strings and buffers while owning text-property composition, exact dependency tracking, retained identity, marker-backed mounts, diffing, transactions, rollback, and final buffer publication.
|
|
|
|
TP does not depend on Ebox or ECSS. It does not implement CSS selectors, stylesheets, specificity, cascade winners, Box, Flex, Grid, measurement, or layout. A CSS consumer may compute final declarations with ECSS and publish them through TP, but TP itself only understands Emacs text properties and generic retained text surfaces.
|
|
|
|
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 2.0 implementation; this
|
|
README remains the conceptual quick start.
|
|
|
|
## Requirements
|
|
|
|
- Emacs 28.1 or newer.
|
|
- No third-party runtime dependency.
|
|
|
|
```elisp
|
|
(add-to-list 'load-path "/path/to/tp")
|
|
(require 'tp)
|
|
```
|
|
|
|
## Choose the smallest public entry point
|
|
|
|
| Need | API | Live runtime? |
|
|
| --- | --- | --- |
|
|
| Return a propertized string | `tp-propertize` | No |
|
|
| Apply declarations once to an existing range | `tp-apply` | No |
|
|
| Reactively decorate existing host text | `tp-watch` | Yes, `properties` capability |
|
|
| Own retained text content | `tp-surface-mount` / `tp-surface-update` | Yes, `content` capability |
|
|
| Inspect or remove a retained publication | `tp-surface-report` / `tp-surface-inspect` / `tp-surface-unmount` | Yes |
|
|
|
|
The one-shot and retained APIs use the same property-policy and projection semantics. One-shot calls deliberately create no object, binding, marker, subscription, or surface state.
|
|
|
|
## Static properties
|
|
|
|
```elisp
|
|
(let* ((callback (lambda (_window _object _position) "Open"))
|
|
(text
|
|
(tp-propertize
|
|
"Hello"
|
|
(list 'face '(:foreground "white" :background "navy")
|
|
'help-echo callback
|
|
'keymap nil))))
|
|
text)
|
|
```
|
|
|
|
Explicit `nil` and an absent property are different. In the example above, `keymap` is present with value `nil`. Function values are literal data, so TP preserves `callback` instead of calling it.
|
|
|
|
To modify an existing range without changing its text:
|
|
|
|
```elisp
|
|
(with-current-buffer (get-buffer-create "*tp-demo*")
|
|
(erase-buffer)
|
|
(insert "abcdef")
|
|
(tp-apply (current-buffer) 2 5 '(face italic)))
|
|
```
|
|
|
|
The established `tp-set`, `tp-reset`, `tp-add`, `tp-remove`, `tp-clear`, `tp-get`, `tp-at`, `tp-member`, match, regexp, search, navigation, interval, and native query APIs remain the direct façade for ordinary text-property work.
|
|
|
|
## Reusable declaration recipes
|
|
|
|
`define-tp` and `define-tps` define reusable recipes that expand to direct Emacs properties. They are definition-time conveniences, not live mounted layers, and they never write identity or provenance into displayed text.
|
|
|
|
```elisp
|
|
(define-tp link-style (foreground)
|
|
`(face (:foreground ,foreground :weight bold)
|
|
mouse-face highlight
|
|
help-echo "Open item"))
|
|
|
|
(tp-set "Project" '(link-style "#58a6ff"))
|
|
```
|
|
|
|
Use `tp-computed` when a property value must be evaluated. Every ordinary function object remains literal.
|
|
|
|
```elisp
|
|
(defvar my-height 1.2)
|
|
|
|
(define-tp sized-label ()
|
|
`(face ,(tp-computed (lambda () (list :height my-height)))))
|
|
```
|
|
|
|
The computed function runs in the current binding/prepare context, so `tp-signal-read` and `tp-binding-read` establish exact dependencies. Its returned value is normalized and projected once, then treated as literal data.
|
|
|
|
## Reactive existing text
|
|
|
|
`tp-watch` decorates a marker-backed host range without owning or replacing its text:
|
|
|
|
```elisp
|
|
(let ((online (tp-signal-create nil)))
|
|
(with-current-buffer (get-buffer-create "*tp-status*")
|
|
(erase-buffer)
|
|
(insert "offline")
|
|
(tp-watch
|
|
(current-buffer) 1 8
|
|
(lambda ()
|
|
(list 'face
|
|
(list :foreground
|
|
(if (tp-signal-read online) "green" "red")))))))
|
|
```
|
|
|
|
A signal records the binding that actually reads it. Updating one signal invalidates only its subscribers; conditional computations automatically stop subscribing to sources that the new branch no longer reads. Equal writes are no-ops, and repeated writes inside `tp-with-transaction` recompute each affected binding at most once.
|
|
|
|
`tp-watch` returns the underlying surface handle. Pass it to `tp-surface-inspect`, `tp-surface-report`, or `tp-surface-unmount`.
|
|
|
|
## Retained content
|
|
|
|
A retained producer receives a prepare context, ensures stable objects before producing output, installs any object-local bindings, and returns a pure surface plan:
|
|
|
|
```elisp
|
|
(let* ((status (tp-signal-create "ready"))
|
|
(producer
|
|
(lambda (context)
|
|
(let* ((object (tp-object-ensure context nil 'status 'text))
|
|
(binding
|
|
(tp-bind object '(demo . status)
|
|
(lambda () (tp-signal-read status)))))
|
|
(tp-surface-plan-create
|
|
:key 'status
|
|
:kind 'text
|
|
:text (tp-binding-read binding)
|
|
:props '(face bold)
|
|
:capability 'content))))
|
|
(buffer (get-buffer-create "*tp-retained*"))
|
|
(surface
|
|
(tp-surface-mount buffer producer '(:capability content))))
|
|
(tp-signal-set status "done")
|
|
(tp-surface-report surface))
|
|
```
|
|
|
|
Plan 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.
|
|
|
|
`tp-surface-materialize-string` uses the same producer and plan semantics without creating a live surface. Its candidate objects, bindings, subscriptions, and anchors are released before it returns.
|
|
|
|
## Host ranges and property ownership
|
|
|
|
The `properties` capability uses opaque range anchors. A producer creates or receives a `tp-range-anchor-create` handle and attaches it to an object with `tp-object-attach-range`. Markers follow host edits; the plan itself remains position-free.
|
|
|
|
TP records the host baseline and each TP contribution per property interval. Overlapping contributions are composed through the registered property policy. If external code changes a property after TP publishes it, the next update reports `tp-property-conflict` instead of overwriting the external value. `tp-range-rebase` explicitly accepts the current host value as the new baseline. Unmount restores only values still owned by TP and preserves conflicting host edits.
|
|
|
|
## Transactions and failures
|
|
|
|
`tp-with-transaction` batches signal writes and all affected surfaces. TP prepares every candidate first, then publishes surfaces in stable order. A compute, validation, buffer write, marker/index publication, or transaction participant failure restores signal values, binding values and dependencies, text, properties, markers, indexes, plans, client state, revisions, and previous reports together.
|
|
|
|
Observers run only after a successful commit. Their failures are recorded and do not roll back an already committed transaction. A buffer killed during publication remains killed; rollback never recreates it.
|
|
|
|
TP 2.0 drives the single live publication from exact transaction-scoped batch
|
|
entries, one frozen participant vector, and the candidate-bound final accept.
|
|
The same journals, surface snapshots, and change group are shared rather than
|
|
copied. Transaction participants register through
|
|
`tp-transaction-participate-v2`; TP has no alternate transaction writer or
|
|
runtime route switch.
|
|
Generic opaque authority markers are bounded and whitelist-validated before
|
|
final accept, then reverse-restored before ordinary rollback on partial apply or
|
|
accept failure. `tp-with-transaction` still returns its body value, and
|
|
internal outcomes remain observational side-channel evidence.
|
|
|
|
ETAF integration uses the same boundary through one opaque transaction
|
|
participant. ETAF prepares its immutable generation and Ebox candidate before
|
|
TP accepts the transaction; participant publish, TP final accept, and
|
|
post-accept cleanup are ordered explicitly. A participant failure restores the
|
|
previous generation and client state, while a post-accept observer failure is
|
|
contained as a diagnostic. ETAF's runtime fixed-point guard records each
|
|
effect's input/version tuple for one flush and stops repeated or over-bound
|
|
cycles instead of spinning.
|
|
|
|
## Property policies
|
|
|
|
`tp-define-property-policy` registers generic semantics for a final Emacs text property:
|
|
|
|
- normalization and validation;
|
|
- equality for no-op detection;
|
|
- contribution merging;
|
|
- projection to the final property value;
|
|
- explicit presence, including present `nil`.
|
|
|
|
`tp-register-text-property` supplies the default policy for a native property. `face` contributions use merge semantics; other properties use their registered policy. `tp-merge-declarations`, `tp-define-style`, and `tp-style-declarations` operate only on direct declarations. They do not implement CSS cascade.
|
|
|
|
## Public runtime families
|
|
|
|
| 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` |
|
|
| 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-v2`, read-only `tp-transaction-active-p`, `tp-runtime-manifest`, 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 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
|
|
|
|
TP 1.0 removes the 0.3 managed stack/renderer runtime instead of hiding it behind compatibility branches. Removed behavior includes `tp-render.el`, `tp-stack.el`, stack mutation APIs, `tp-text`, `$variable` declarations, layer-to-buffer registries, scan-driven refresh, managed attach/detach/diagnostics, and inline `tp-name`/`tp-layers`/`tp-meta` runtime storage.
|
|
|
|
## TP 2.0 transaction migration
|
|
|
|
TP 2.0 removes the v1 transaction participant facade and the legacy execution
|
|
route. Replace `(tp-transaction-participate KEY PUBLISH ROLLBACK)` exactly with
|
|
`(tp-transaction-participate-v2 :key KEY :stage PUBLISH :rollback ROLLBACK)`.
|
|
Callers that inspect `tp-runtime-manifest` must require
|
|
`tp-transaction-protocol-v2`; route-selection and v1-adapter fields are no
|
|
longer published.
|
|
|
|
Use direct recipes for reusable static declarations, `tp-watch` for reactive properties on existing text, and retained content surfaces for reactive text or structured UI. TP does not automatically scan historical propertized text to reconstruct runtime identity.
|
|
|
|
## Examples
|
|
|
|
- [Static properties](examples/static-properties.el)
|
|
- [Reactive status](examples/reactive-status.el)
|
|
- [Retained dashboard](examples/retained-dashboard.el)
|
|
- [Diagnostic decoration](examples/diagnostic-decoration.el)
|
|
|
|
These examples use only TP public APIs and are tested without Ebox or ECSS on the load path.
|
|
|
|
## Verification
|
|
|
|
```sh
|
|
make test
|
|
make test-shuffled SHUFFLE_SEED=20260806
|
|
make doctest
|
|
make compile-all WERROR=t
|
|
make checkdoc
|
|
make package-lint
|
|
make diff-check
|
|
```
|
|
|
|
The structural tests also prove that normal signal and surface updates do not scan `buffer-list` or search displayed text for identity, equal results do not publish a new revision, and unmount/kill cleanup releases subscriptions and marker-backed runtime state.
|
|
|
|
## License
|
|
|
|
GPL-3.0-or-later. See [LICENSE](LICENSE).
|