- Emacs Lisp 95.6%
- Python 3.9%
- Makefile 0.5%
|
Some checks failed
Repository structure / structure (push) Has been cancelled
Keep candidate-owned target mount IDs, operation counts and outcome metadata shared through publication. Public report snapshots still defensively copy values, while rollback journals and all precommit/shadow validators remain unchanged. This removes repeated property graph copies from the publication hot path without creating a second authority. Validation: make -C tp check passed (120 public cases and API boundary check). Integrated workspace checks passed. Added a metadata-only update/rollback regression covering output, revision, client state and mount identity. |
||
|---|---|---|
| .githooks | ||
| .github/workflows | ||
| benchmarks | ||
| docs | ||
| examples | ||
| lisp | ||
| scripts | ||
| tests | ||
| .editorconfig | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| README.zh-CN.md | ||
| tp.el | ||
TP
Source layout: add the repository root to load-path and use
(require 'tp). The supported API and usage notes are in
tp.el; implementation modules live in lisp/.
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.zh-CN.md.
Complete public API reference: 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.
(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
(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:
(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.
(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.
(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:
(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:
(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 for signatures, return shapes, examples, and the full module index. See API semantics for ownership, lifecycle, error, and return contracts, and architecture 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
These examples use only TP public APIs and are tested without Ebox or ECSS on the load path.
Verification
make test
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.
Benchmarking
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.
Run
Use the Makefile entry point:
make benchmark
The equivalent batch command is:
emacs -Q --batch -L lisp -l benchmarks/tp-benchmark.el -f tp-benchmark-run
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.
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 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, architecture, and the public API reference.
Development
After cloning, run make setup-hooks. Before submitting a change, run make check; make structure-check is the fast organization gate.
make check runs structure checks, compilation and the public acceptance scenarios listed in tests/acceptance.json. make test runs the same public API suite. The inventory covers every maintained Lisp test. GUI and performance checks use separate targets.
Source layout
Add the repository root to load-path and require tp. The root entry
loads the implementation in lisp/ and documents the supported public APIs in
its Commentary. Package archives include both the root entry and lisp/.