Add canonical query semantics, managed metadata and transactions, overlay-aware lookup, reproducible benchmarks, and synchronized API documentation. |
||
|---|---|---|
| .github/workflows | ||
| docs | ||
| postmortem | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| Makefile | ||
| README_CN.md | ||
| README.md | ||
| tp-benchmark.el | ||
| tp-builtins-tests.el | ||
| tp-builtins.el | ||
| tp-char-tests.el | ||
| tp-core-tests.el | ||
| tp-core.el | ||
| tp-doctest.el | ||
| tp-layer-tests.el | ||
| tp-layer.el | ||
| tp-managed-tests.el | ||
| tp-native-tests.el | ||
| tp-ops-tests.el | ||
| tp-ops.el | ||
| tp-palette.el | ||
| tp-query.el | ||
| tp-reactive.el | ||
| tp-render-tests.el | ||
| tp-render.el | ||
| tp-run-shuffled.el | ||
| tp-search-tests.el | ||
| tp-search.el | ||
| tp-stack-tests.el | ||
| tp-stack.el | ||
| tp-tests.el | ||
| tp.el | ||
tp.el - Text Properties Library for Emacs
A powerful text properties manipulation library with an innovative property layer system
Features • Installation • Quick Start • API Reference • Property Layer System • Reactive Text Properties • 中文文档
Table of Contents
- Quick Start
- Overview
- Features
- Requirements
- Installation
- API Reference
- The Property Layer System
- Custom Text Properties
- Text Property Layers
- Property Layer Concept
- Property Layer Definition
- Property Layer Placement
- Property Layer Deletion
- Property Layer Movement
- Property Layer Visibility
- Managed Layer Lifecycle
- Property Layer Merging
- Property Layer Query Functions
- Utility Functions
- Color Palette System
- Reactive Text Properties
- Core Concept
- How It Works
- Defining Reactive Layers
- :data - Additional Reactive State
- :compute - Computed Properties
- :watch - Side Effect Callbacks
- :transform - Value Transformation
- Anonymous Reactive Layers
- Layer Name Resolution in APIs
- Reactive Layer Groups
- Batched Updates
- Layer-Buffer Registry & Lifecycle
- Debug Mode
- Resetting Reactive State
- Complete Example: Theme-Aware Text
- Practical Examples
- License
- Contributing
Quick Start
;; Install: clone the repository, add it to your load-path, and require
(add-to-list 'load-path "/path/to/tp")
(require 'tp)
;; Set properties with one unified API (returns a new propertized string)
(tp-set "hello" 'face 'bold)
;; => #("hello" 0 5 (face bold))
;; Stack property layers on a buffer region
(define-tp spotlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 6 'spotlight)
(tp-layer-top 1 6))
;; => spotlight
;; Reactive: text properties follow a variable
(defvar accent-color "red")
(define-tp accent ()
:props '(face (:foreground $accent-color)))
(with-temp-buffer
(insert "Hello")
(tp-push-layer 1 6 'accent)
(setq accent-color "blue") ; text updates automatically!
(tp-at 1 'face))
;; => (:foreground "blue")
Overview
tp.el is a library that comprehensively enhances Emacs text property manipulation. It is not just a simple wrapper around native text property APIs (like put-text-property, get-text-property), but provides many functional extensions that native functions do not have. tp.el innovates in the following areas:
Since 0.2.0 the library is organized as a family of layered modules (tp-core, tp-reactive, tp-layer, tp-ops, tp-search, tp-render, tp-stack, tp-palette, tp-builtins) behind the umbrella file tp.el — (require 'tp) still loads everything, so nothing changes for users. See Installation for the module map.
Core Innovations
- Unified API Parameter Conventions: All functions support multiple flexible calling patterns, working seamlessly with both strings and buffers
- Fine-grained Sub-property Operations: Support path-style access, modification, and deep merging of nested properties
- Innovative Property Layer System: Stack and manage multiple sets of properties on the same text region with layered control
- 🆕 Reactive Text Properties: Automatically update text properties when variable values change - a groundbreaking feature inspired by modern reactive UI frameworks
- Pattern Matching Batch Operations: Batch apply properties via string or regular expression matching
- Enhanced Search & Navigation: Rich property search and traversal functionality
Features
Unified API Parameter Conventions
Native Emacs APIs have different functions and parameter orders for strings and buffers. tp.el unifies all of this:
- ✅ Three Calling Conventions: All core functions (
tp-set,tp-get,tp-remove, etc.) support three flexible calling patterns:;; 1. Current buffer (tp-set START END '(face bold)) ;; 2. Specific buffer or string (tp-set START END '(face bold) OBJECT) ;; 3. Entire string (flat properties or layer name) (tp-set STRING 'face 'bold 'help-echo "tip") (tp-set STRING 'layer-name) - ✅ Unified Object Support: The same function works with both strings and buffers, no need to remember different APIs
One rule to remember: when the first argument is a string, the call
operates on that whole string; when it is a number, the call operates on
the [START, END) region of OBJECT — and OBJECT always comes last (nil means
the current buffer). Every core and layer-stack function follows this rule.
The match/search family (tp-match-*, tp-regexp-*, tp-search-map,
tp-forward-do/tp-backward-do) follows a deliberate second convention:
PATTERN (or FUNCTION) and PLIST come first, then OBJECT, then the optional
START/END bounds. Operating on the whole object is these functions' common
case, so OBJECT sits before the range instead of after it.
Return value conventions (as of 0.3.0):
| Family | Return value |
|---|---|
tp-set / tp-reset / tp-add |
(START . END) for buffer/region forms; a new string for whole-string forms |
tp-remove |
nil for buffer forms; a new string for whole-string forms |
tp-clear |
nil |
tp-match-* / tp-regexp-* |
list of (START . END) matches for buffers; a new string for strings |
| Stack mutators (delete/pop/move/raise/lower/rotate/pin/switch/hide/show/merge/flatten) | the number of property runs modified (0 = nothing matched) |
tp-put-layer / tp-push-layer |
OBJECT when given (the string itself in string forms), else (START . END) |
tp-add-to-layers / tp-add-to-all-layers |
the string itself (mutated in place) for string forms; nil for buffers |
The precise object, mutation, nil/presence, search, and managed-layer contracts are centralized in docs/API-SEMANTICS.md. “Unified” means one high-level vocabulary; a few documented string/buffer return differences remain for compatibility.
Namespace map: tp-layer-NAME functions taking a layer name argument
(tp-layer-props, tp-layer-arglist, ...) query the layer registry
(definitions); the ones taking position arguments — START END
(tp-layer-list, tp-layer-count, tp-layer-top, ...) or a single POS
(tp-layer-stack-at) — query the layer stack on actual text.
Naming conventions: tp-define-layer / tp-define-group /
tp-define-palette are the prefix-conforming canonical names going forward
(discoverable via C-h f tp-...); define-tp / define-tps /
define-tp-group / define-tp-palette are permanent aliases that will never
be removed (this README's examples still use the historical names).
tp-search-forward / tp-search-backward are deprecated since 0.3.0 — see
Search & Navigation.
Three Property Operation Semantics
Native APIs only have simple set and get. tp.el provides three clear operation semantics:
- ✅
tp-reset: Complete replacement - clears all existing properties, sets new ones - ✅
tp-set: Partial replacement - only replaces specified properties, preserves others - ✅
tp-add: Deep merge - intelligently merges nested properties instead of simple overwrite
;; Deep merge example
(tp-set 1 10 '(face (:foreground "red")))
(tp-add 1 10 '(face (:background "blue")))
;; Result: face is (:foreground "red" :background "blue")
;; Native API would completely overwrite, but tp-add merges intelligently
Fine-grained Sub-property Operations
This is functionality that native APIs completely lack. tp.el supports fine-grained reading, modification, and deletion of nested properties:
- ✅ Path-style Access: Access deeply nested property values through path syntax
;; Get nested properties (tp-get returns (START END VALUE) intervals) (tp-get str 'face :underline :style) ; => ((0 5 wave)) (tp-at 5 '(face :box :color)) ; => "blue" ;; Get multiple nested keys (tp-get str 'face :underline '(:color :style)) ;; => ((0 5 (:color "green" :style wave))) - ✅ Sub-property Deletion: Precisely remove specific keys from nested properties
;; Only delete :style from :underline, preserve :color (tp-remove 1 10 '(face :underline :style)) - ✅ Deep Merge:
tp-addrecursively merges nested plist structures - ✅ Smart Face Merging: Symbol faces are automatically prepended to face lists, plist faces are deep merged
- ✅ Automatic Duplicate Property Merging in Single Call: When the same property (e.g.,
face) is specified multiple times in a singletp-set/tp-add/tp-resetcall, they are automatically merged
;; Merge multiple faces in a single call
(tp-set "emacs"
'face 'bold
'face '(:background "green")
'face '(:foreground "red"))
;; Result: face is ((:foreground "red") (:background "green") bold)
;; (entries stack into one face list, most recent first)
;; Later values override earlier ones for the same sub-property
(tp-set "emacs"
'face '(:foreground "red")
'face '(:foreground "yellow"))
;; Result: foreground is "yellow"
;; Use with tp-palette layer
(tp-set "emacs"
'tp-palette 'info
'face '(:foreground "red"))
;; Result: tp-palette's face is merged with (:foreground "red")
Innovative Property Layer System
This is tp.el's most innovative feature, completely unsupported by native Emacs. The property layer system allows stacking multiple sets of properties on the same text region:
- ✅ Property Layer Stack Concept: Multiple property layers stack like a stack, only the top layer is visible, lower layers are preserved
- ✅ Property Layer Definition & Reuse: Define reusable property layers and layer groups via
define-tpanddefine-tps - ✅ Rich Property Layer Operations:
- Placement:
tp-put-layer(specific position),tp-push-layer(top) - Deletion:
tp-delete-layer(by name/index),tp-pop-layer(top layer) - Movement:
tp-raise-layer/tp-lower-layer(up/down),tp-rotate-layer(rotate),tp-pin-layer(one-shot move to top),tp-switch-layer(swap) - Visibility:
tp-hide-layer/tp-show-layer(hide a layer without removing it) - Merging:
tp-merge-layers(merge specified layers),tp-flatten-layers(flatten all layers)
- Placement:
- ✅ Property Layer Queries:
tp-layer-list,tp-layer-count,tp-layer-exists-p,tp-layer-top,tp-layer-stack-at
;; Property layer usage example
(define-tp highlight () '(face (:background "yellow")))
(define-tp error () '(face (:foreground "red")))
;; Stack multiple property layers
(tp-push-layer 1 10 'highlight)
(tp-push-layer 1 10 'error) ; error is now visible
;; Rotate display
(tp-rotate-layer 1 10) ; highlight is now visible
Pattern Matching & Batch Operations
Native APIs require manual searching and looping. tp.el provides convenient pattern matching functionality:
- ✅ String Matching:
tp-match-set,tp-match-reset,tp-match-add - ✅ Regexp Matching:
tp-regexp-set,tp-regexp-reset,tp-regexp-add - ✅ Three Semantic Variants: Each match type supports set/reset/add operation semantics
;; Highlight all TODOs
(tp-match-set "TODO" '(face warning))
;; Regexp match all numbers
(tp-regexp-set "[0-9]+" '(face font-lock-number-face))
;; Add properties with deep merge
(tp-match-add "TODO" '(face (:underline t)))
🆕 Reactive Text Properties
This is tp.el's most innovative new feature - reactive text properties automatically update when variable values change. Inspired by modern reactive UI frameworks like Vue.js, this feature brings reactive programming to Emacs text properties:
- ✅ Reactive Variables: Use
$-prefixed symbols (like$my-color) in property definitions - they automatically resolve to variable values - ✅ Automatic Updates: When a reactive variable changes, all text regions using that variable are automatically updated
- ✅ :data for Additional State: Define additional reactive variables that aren't directly used in properties but can trigger updates
- ✅ :compute for Derived Values: Create computed properties that derive their values from other reactive variables (like Vue's computed properties)
- ✅ :watch for Side Effects: Execute callbacks when reactive variables change (like Vue's watch)
- ✅ Targeted Updates (0.3.0): a layer→buffer registry means updates visit only the buffers showing the affected layer,
tp-textre-renders edit only the differing span (point and markers stay put), andtp-reactive-track-buffer/tp-gc-anonymous-layersmanage the layer lifecycle — see Layer-Buffer Registry & Lifecycle
;; Define a layer with reactive properties
(defvar my-color "red") ;; Reactive variable
(define-tp my-highlight ()
:props '(face (:foreground $my-color)))
;; Apply the layer
(tp-push-layer 1 10 'my-highlight)
;; Later, just change the variable - text updates automatically!
(setq my-color "blue") ;; All text with my-highlight layer updates to blue!
;; Advanced example with :data, :compute, and :watch
;; (note: ARGLIST () is mandatory, and the keyword values are quoted)
(define-tp full-name-layer ()
:props '(help-echo $full-name face (:foreground $name-color))
:data '((first-name . "John") (last-name . "Doe") (name-color . "purple"))
:compute '((full-name (lambda () (concat first-name " " last-name))))
:watch '((first-name (lambda (new old layer)
(message "Name changed from %s to %s" old new)))))
Enhanced Search & Navigation
- ✅ Range Search:
tp-searchreturns a list of all matching intervals - ✅ N-times Search:
tp-forward/tp-backwardsupport searching forward/backward N times, with optional PREDICATE matching and NOT-CURRENT - ✅ Search and Execute:
tp-forward-do/tp-backward-dosearch N times and apply a function at the Nth match - ✅ Batch Transform:
tp-search-mapapplies transformation function to all matches
;; Search all markers
(tp-search my-string 'marker) ; => ((0 5 t) (12 17 t))
;; Upcase all marker text
(tp-search-map #'upcase 'marker tp-any-value my-string)
Requirements
- Emacs 28.1+ (uses
object-intervalsfunction) - dash.el 2.19.1+ (list manipulation utilities)
Installation
The library is the tp-*.el module family plus the umbrella file tp.el.
Installing means putting the directory on your load-path and requiring the
umbrella, which loads every module:
;; Add to your load-path
(add-to-list 'load-path "/path/to/tp")
(require 'tp)
Or with use-package:
(use-package tp
:load-path "/path/to/tp")
The modules and their roles:
| Module | Responsibility |
|---|---|
tp-core.el |
Intervals, plist/face merge engine, debug logging, $var utilities |
tp-reactive.el |
Reactive dependency registry, variable watchers, batching queue |
tp-layer.el |
define-tp / define-tps, layer registry and resolution |
tp-ops.el |
tp-set / tp-reset / tp-add / tp-get / tp-at / tp-remove / tp-clear |
tp-search.el |
tp-match-*, tp-regexp-*, tp-search, navigation |
tp-render.el |
Reactive re-rendering engine |
tp-stack.el |
Layer stack operations (push/pop/move/merge/flatten/...) |
tp-query.el |
Native text lookup/change wrappers and mutation policy |
tp-palette.el |
Light/dark color palette data |
tp-builtins.el |
Built-in layers, palette gallery, display-buffer helpers |
A Makefile is included: make test runs all ERT suites, make doctest
executes the README examples against the code (tp-doctest.el),
make compile byte-compiles the modules, and make clean removes
compiled files.
API Reference
API Quick Reference
A complete overview of all tp.el functions organized by category:
Core Property Functions
| Function | Description |
|---|---|
tp-set |
Set text properties (replaces specified properties only) |
tp-reset |
Replace ALL text properties |
tp-add |
Add/merge properties with deep merge support |
tp-get |
Get property value(s) from range or string |
tp-at |
Get property value(s) at a single position |
tp-member |
Like tp-at, but distinguishes present-with-nil from absent |
tp-remove |
Remove a property or sub-property |
tp-clear |
Clear all text properties from a region |
Pattern Matching Functions
| Function | Description |
|---|---|
tp-match-set |
Set properties on string pattern matches (optional bounds) |
tp-match-reset |
Reset all properties on string matches (optional bounds) |
tp-match-add |
Add/merge properties on string matches (optional bounds) |
tp-regexp-set |
Set properties on regexp matches (optional bounds and capture group) |
tp-regexp-reset |
Reset all properties on regexp matches (optional bounds and capture group) |
tp-regexp-add |
Add/merge properties on regexp matches (optional bounds and capture group) |
Search & Navigation Functions
| Function | Description |
|---|---|
tp-search-forward |
Deprecated (0.3.0) — use tp-forward or the Emacs primitive |
tp-search-backward |
Deprecated (0.3.0) — use tp-backward or the Emacs primitive |
tp-forward |
Search forward N times for text with property (optional predicate matching) |
tp-backward |
Search backward N times for text with property (optional predicate matching) |
tp-forward-do |
Search forward N times, apply function at the Nth match |
tp-backward-do |
Search backward N times, apply function at the Nth match |
tp-search |
Search all matching properties in range or string |
tp-search-map |
Apply function to all matches (with optional start/end range) |
Native Text Property Compatibility
| Function | Description |
|---|---|
tp-lookup |
Text-only direct/effective/source-aware property lookup |
tp-lookup-result |
Result record returned by tp-lookup |
tp-property-change |
Wrapper for next/previous single-property or all-property change positions |
tp-property-any |
Wrapper for text-property-any |
tp-property-not-all |
Wrapper for text-property-not-all |
tp-with-mutation-policy |
Explicit modified/read-only mutation policy wrapper |
Property Layer Definition Functions
| Function | Description |
|---|---|
define-tp |
Define custom text property (layer) with optional parameters |
define-tps |
Define custom text property group (layer group) with optional parameters |
tp-define-layer / tp-define-group |
Prefix-conforming aliases of define-tp / define-tps |
tp-layer-props |
Get properties for a layer |
tp-group-props |
Get properties for all layers in a group |
tp-layer-props-with-args |
Expand a parameterized layer with a list of arguments |
tp-group-props-with-args |
Expand a parameterized group with a list of arguments |
tp-layer-arglist |
Get a parameterized layer's parameter list |
tp-describe-layer |
Describe a layer's definition in a help buffer |
tp-undefine-layer |
Remove layer definition |
tp-undefine-group |
Remove group definition |
tp-layer-reset |
Clear all layer/group definitions |
tp-reactive-reset |
Clear all reactive dependencies and watchers |
Property Layer Placement Functions
| Function | Description |
|---|---|
tp-put-layer |
Set layer at specific index position (optional NOERROR) |
tp-push-layer |
Push layer to top of stack (optional NOERROR) |
Property Layer Deletion Functions
| Function | Description |
|---|---|
tp-delete-layer |
Delete layer by name or index |
tp-pop-layer |
Remove top layer |
Property Layer Movement Functions
| Function | Description |
|---|---|
tp-move-layer |
Move a layer from one position to another |
tp-raise-layer |
Move layer up/down by N positions |
tp-lower-layer |
Mirror of tp-raise-layer: move layer down/up by N positions |
tp-rotate-layer |
Cycle layers up or down by N steps |
tp-pin-layer |
Move a layer to the top (one-shot; later pushes can cover it) |
tp-switch-layer |
Swap positions of two layers |
Property Layer Visibility Functions
| Function | Description |
|---|---|
tp-hide-layer |
Hide a layer without removing it from the stack |
tp-show-layer |
Make a hidden layer render again |
Managed Layer Lifecycle
| Function | Description |
|---|---|
tp-attach-managed-layers |
Attach inserted/copied managed layer storage to the current buffer registry |
tp-detach-managed-layers |
Remove managed storage, optionally keeping rendered visible properties |
tp-managed-layer-diagnostics |
Read-only diagnostics for one layer |
tp-managed-buffer-diagnostics |
Read-only diagnostics for one buffer |
tp-managed-diagnostics |
Read-only global managed lifecycle diagnostics |
tp-layer-transaction |
Run a managed stack change with rollback on error |
Property Layer Merging Functions
| Function | Description |
|---|---|
tp-merge-layers |
Merge specified layers into a new layer (hidden layers contribute no props) |
tp-flatten-layers |
Flatten all layers into a single layer (hidden layers are discarded) |
Property Layer Query Functions
| Function | Description |
|---|---|
tp-layer-list |
List all layer names in region |
tp-layer-count |
Count layers in region |
tp-layer-exists-p |
Check if layer exists in region |
tp-layer-top |
Get name of top layer (in stack order, even when hidden) |
tp-layer-stack-at |
Full ordered stack at one position as (NAME . PROPS) conses |
tp-region-layer-props |
Get properties for a specific layer in region |
Property Layer Manipulation Functions
| Function | Description |
|---|---|
tp-add-to-layers |
Add/merge properties to specific layers by index or name |
tp-add-to-all-layers |
Add/merge properties to all existing layers |
Utility Functions
| Function | Description |
|---|---|
tp-intervals |
Get all text property intervals in a region (optional ABSOLUTE coordinates) |
tp-intervals-map |
Apply function to all intervals in a region (optional ABSOLUTE coordinates) |
tp-plist |
Get all properties present in a region |
tp-empty-p |
Check if object has no text properties |
tp-with-current-buffer |
Run body in a buffer with inhibit-read-only bound |
tp-pop-to-buffer |
Fill a buffer, make it read-only, display via pop-to-buffer |
tp-switch-to-buffer |
Fill a buffer, make it read-only, display via switch-to-buffer |
Palette Functions
| Function | Description |
|---|---|
tp-palette-alist |
Registry of named palettes (variable) |
define-tp-palette |
Register or update a named palette (alias: tp-define-palette) |
tp-palette-color |
Get a palette's :fg / :bg / :border color, theme-resolved |
tp-palette-has-p |
Test whether a palette (or one of its keys) is defined |
tp-palette-show |
Show a gallery of all registered palettes |
tp-parse-color |
Resolve a color spec for the current light/dark theme |
Reactive Lifecycle Functions
| Function | Description |
|---|---|
tp-with-batch-updates |
Apply several reactive variable changes as one update |
tp-reactive-layer-buffers |
Buffers registered as showing a layer (or unknown) |
tp-reactive-track-buffer |
Register a buffer after inserting an already-propertized string |
tp-gc-anonymous-layers |
Collect anonymous layers no registered live buffer still shows |
Core Property Functions
Important: String Modification Behavior
The core property functions (
tp-set,tp-reset,tp-add,tp-remove) have different behaviors depending on the calling convention:
Calling Convention Underlying Implementation Modifies Original? (tp-set STRING PROP VAL ...)Uses propertizeinternallyNo - Returns a NEW string (tp-set START END PROPS)Uses put-text-propertyon bufferYes - Modifies current buffer (tp-set START END PROPS STRING)Uses put-text-propertyon stringYes - Modifies original string (tp-set START END PROPS BUFFER)Uses put-text-propertyon bufferYes - Modifies the buffer Summary:
- Entire string form
(tp-set "string" ...): Creates a new propertized string. The original string is not modified. This usespropertizeinternally.- Region form with string object
(tp-set 0 5 '(...) string): Directly modifies the original string object usingput-text-propertyorset-text-properties.- Buffer forms: Always modify the buffer in-place.
This distinction applies to all core property functions:
tp-set,tp-reset,tp-add, andtp-remove.
tp-set - Set Text Properties
Set text properties on a string or buffer region. Replaces only the specified properties, preserving others.
;; Current buffer (properties as a list) - modifies buffer in-place
(tp-set START END '(PROPERTY VALUE ...))
(tp-set START END LAYER-NAME)
;; Specific buffer or string - modifies OBJECT in-place
(tp-set START END '(PROPERTY VALUE ...) OBJECT)
(tp-set START END LAYER-NAME OBJECT)
;; Entire string (flat properties or layer name) - returns NEW string
(tp-set STRING PROPERTY VALUE ...)
(tp-set STRING LAYER-NAME)
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
Return Values:
- Buffer forms: Returns
(START . END)cons cell - String region form
(tp-set 0 5 '(...) string): Returns the modified string (same object) - Entire string form
(tp-set "string" ...): Returns a new propertized string
Examples:
;; Set face on buffer region
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold)))
;; => (1 . 10)
;; Set multiple properties
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold help-echo "Click me")))
;; => (1 . 10)
;; Use a defined layer name
(define-tp warning-style ()
'(face (:foreground "orange" :weight bold)))
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 'warning-style))
;; => (1 . 10)
;; Set on specific buffer
(let ((my-buffer (generate-new-buffer "*test*")))
(with-current-buffer my-buffer
(insert "Hello World"))
(prog1 (tp-set 1 10 '(face italic) my-buffer)
(kill-buffer my-buffer)))
;; => (1 . 10)
;; Set properties on a string region (0-indexed) - MODIFIES original string
(let ((my-string (copy-sequence "Hello World")))
(tp-set 0 5 '(face italic) my-string)
my-string)
;; => #("Hello World" 0 5 (face italic))
;; Set properties on entire string - returns NEW string, original unchanged
(let ((original "Hello"))
(let ((result (tp-set original 'face 'bold)))
(list :original original
:result result
:original-has-props (get-text-property 0 'face original)
:result-has-props (get-text-property 0 'face result))))
;; => (:original "Hello" :result #("Hello" 0 5 (face bold))
;; :original-has-props nil :result-has-props bold)
;; Use a defined layer name on entire string
(define-tp my-style ()
:props '(face (:foreground $my-color))
:data '((my-color . "blue")))
(tp-set " " 'my-style)
;; => #(" " 0 1 (face (:foreground "blue") tp-name my-style))
;; (the printed ORDER of properties may differ across Emacs
;; versions; the values are identical)
;; Merge multiple faces in a single call (duplicate properties auto-merged)
(tp-set "emacs"
'face 'bold
'face '(:background "green")
'face '(:foreground "red"))
;; => face is ((:foreground "red") (:background "green") bold)
;; (entries stack into one face list, most recent first)
;; Later values override earlier ones for the same sub-property
(tp-set "emacs"
'face '(:foreground "red")
'face '(:foreground "yellow"))
;; => face's :foreground is "yellow" (later overrides earlier)
;; Use with tp-palette layer, merging extra face properties
(tp-set "emacs"
'tp-palette 'info
'face '(:foreground "red"))
;; => tp-palette's face is merged with (:foreground "red"), :foreground is overridden
tp-reset - Replace All Properties
Completely replace ALL text properties with the specified ones.
;; Buffer/region forms - modifies in-place
(tp-reset START END '(PROPERTY VALUE ...) &optional OBJECT)
(tp-reset START END LAYER-NAME &optional OBJECT)
;; Entire string form - returns NEW string
(tp-reset STRING PROPERTY VALUE ...)
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
Return Values:
- Buffer forms: Returns
(START . END)cons cell - String region form: Returns the modified string (same object)
- Entire string form: Returns a new propertized string
Examples:
;; Replace all properties in region
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(help-echo "old")) ; Set existing property
(tp-reset 1 10 '(face bold)) ; Any existing properties are removed
(tp-at 1))
;; => (face bold) ; help-echo is gone
;; On entire string - returns NEW string, original unchanged
(let ((original "Hello"))
(let ((result (tp-reset original 'face 'italic)))
(list :original-modified (get-text-property 0 'face original)
:result-face (get-text-property 0 'face result))))
;; => (:original-modified nil :result-face italic)
;; Use a defined layer name
(define-tp error-style ()
'(face (:foreground "red" :weight bold)))
(with-temp-buffer
(insert "Hello World")
(tp-reset 1 10 'error-style))
;; => (1 . 10) ; All properties replaced with error-style
tp-add - Add/Merge Properties
Add or update properties with deep merge support for nested plists.
;; Buffer/region forms - modifies in-place
(tp-add START END '(PROPERTY VALUE ...) &optional OBJECT)
(tp-add START END LAYER-NAME &optional OBJECT)
;; Entire string form - returns NEW string
(tp-add STRING PROPERTY VALUE ...)
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
Return Values:
- Buffer forms: Returns
(START . END)cons cell - String region form: Returns the modified string (same object)
- Entire string form: Returns a new propertized string
Examples:
;; Add properties (preserves existing, merges nested)
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold))
(tp-add 1 10 '(help-echo "tooltip"))
(tp-at 1))
;; => (face bold help-echo "tooltip")
;; Deep merge face properties
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face (:foreground "red")))
(tp-add 1 10 '(face (:background "blue")))
(tp-at 1 'face))
;; => (:foreground "red" :background "blue")
;; Entire string form - returns NEW string, original unchanged
(let ((original "Hello"))
(let ((result (tp-add original 'face 'bold)))
(list :original-modified (get-text-property 0 'face original)
:result-face (get-text-property 0 'face result))))
;; => (:original-modified nil :result-face bold)
;; Use a defined layer name
(define-tp highlight-style ()
'(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold))
(tp-add 1 10 'highlight-style)
(tp-at 1))
;; => Properties merged with highlight-style
tp-get - Get Property Value
Get property value(s) from range or string, with support for nested sub-properties.
Returns a list of (START END VALUE) intervals, allowing you to see all property values across the range.
For single position queries, use tp-at instead.
;; Range - specific property (returns list of intervals)
(tp-get START END PROPERTY)
(tp-get START END PROPERTY OBJECT)
;; Range with property path as list
(tp-get START END '(PROPERTY) OBJECT)
(tp-get START END '(PROPERTY SUB-KEY ...) OBJECT)
;; Range with deeply nested property path
(tp-get START END '(PROPERTY SUB-KEY SUB-SUB-KEY ...) OBJECT)
;; Range extracting multiple keys from nested property
(tp-get START END '(PROPERTY SUB-KEY (KEY1 KEY2 ...)) OBJECT)
;; Range - all properties (returns list of intervals)
(tp-get START END)
(tp-get START END OBJECT)
;; Entire string (returns list of intervals)
(tp-get STRING)
(tp-get STRING PROPERTY)
(tp-get STRING PROPERTY SUB-KEY ...)
(tp-get STRING PROPERTY SUB-KEY '(KEY1 KEY2 ...))
(tp-get STRING '(PROPERTY SUB-KEY ...))
Examples:
;; Get from range - returns list of (START END VALUE) intervals
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
(tp-get 1 10 'face))
;; => ((1 6 bold))
;; Get with multiple intervals
(let ((str (copy-sequence "Hello World Hello")))
(tp-set 0 5 '(face bold) str)
(tp-set 12 17 '(face italic) str)
(tp-get 0 17 'face str))
;; => ((0 5 bold) (12 17 italic))
;; Get with property path as list
(let ((my-string (copy-sequence "Hello World Hello World")))
(tp-set 5 20 '(face (:underline (:style wave))) my-string)
(tp-get 5 20 '(face :underline :style) my-string))
;; => ((5 20 wave))
;; Get deeply nested property from entire string
(let ((str (copy-sequence "Hello World")))
(tp-set 0 5 '(face (:underline (:color "green"))) str)
(tp-set 6 11 '(face (:underline (:color "yellow"))) str)
(tp-get str 'face :underline :color))
;; => ((0 5 "green") (6 11 "yellow"))
;; Get multiple keys from nested property
(let ((str (copy-sequence "Hello World")))
(tp-set 0 5 '(face (:underline (:color "green" :style wave))) str)
(tp-set 6 11 '(face (:underline (:color "yellow" :style line))) str)
(tp-get str 'face :underline '(:color :style)))
;; => ((0 5 (:color "green" :style wave)) (6 11 (:color "yellow" :style line)))
;; Get all properties from range
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold help-echo "test"))
(tp-get 1 10))
;; => ((1 6 (face bold help-echo "test")))
;; Get from entire string - returns list of intervals
(let ((str (copy-sequence "Hello World Hello")))
(tp-set 0 5 '(face bold) str)
(tp-set 12 17 '(face italic) str)
(list (tp-get str) ; => ((0 5 (face bold)) (12 17 (face italic)))
(tp-get str 'face))) ; => ((0 5 bold) (12 17 italic))
;; => (((0 5 (face bold)) (12 17 (face italic))) ((0 5 bold) (12 17 italic)))
tp-at - Get Property at Position
;; Get all properties at position
(tp-at POS)
(tp-at POS OBJECT)
;; Get specific property at position
(tp-at POS PROPERTY)
(tp-at POS PROPERTY OBJECT)
;; Get nested sub-property at position
(tp-at POS '(PROPERTY SUB-KEY ...))
(tp-at POS '(PROPERTY SUB-KEY ...) OBJECT)
Get text properties at POS, optionally filtered by PROPERTY.
For single-position property queries (previously done with tp-get), use tp-at.
Examples:
;; Get all properties at position 5 in current buffer
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold help-echo "test"))
(tp-at 5))
;; => (face bold help-echo "test")
;; Get all properties at position 0 in string
(let ((my-string (tp-set "Hello" 'face 'italic 'help-echo "greeting")))
(tp-at 0 my-string))
;; => (face italic help-echo "greeting")
;; Get specific property at position
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold))
(tp-at 5 'face))
;; => bold
;; Get specific property at position in string
(let ((my-string (tp-set "Hello" 'face 'italic)))
(tp-at 0 'face my-string))
;; => italic
;; Get nested sub-property at position
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face (:foreground "red" :box (:color "blue"))))
(list (tp-at 5 '(face :foreground))
(tp-at 5 '(face :box :color))))
;; => ("red" "blue")
;; Get nested sub-property from string
(let ((str (copy-sequence "Hello")))
(tp-set 0 5 '(face (:foreground "red" :underline t)) str)
(tp-at 0 '(face :foreground) str))
;; => "red"
tp-member - Property Membership at Position
(tp-member POS PROPERTY &optional OBJECT)
Like tp-at, but returns a (PROPERTY VALUE) list when PROPERTY is present
at POS, or nil when it is absent. This distinguishes a property that is
present with the value nil from a property that is missing entirely
(analogous to plist-member).
Examples:
;; Present with value nil vs. absent
(let ((str (copy-sequence "Hello")))
(tp-set 0 5 '(face nil) str)
(list (tp-member 0 'face str) ; present, value nil
(tp-member 0 'display str))) ; absent
;; => ((face nil) nil)
;; In a buffer
(with-temp-buffer
(insert "Hello")
(tp-set 1 6 '(face bold))
(tp-member 1 'face))
;; => (face bold)
tp-remove - Remove Property
Remove a property or nested sub-property from a region or entire string.
;; Remove entire property (buffer) - modifies in-place
(tp-remove START END PROPERTY &optional OBJECT)
;; Remove sub-property (buffer) - modifies in-place
(tp-remove START END '(PROPERTY SUB-KEY) &optional OBJECT)
;; Remove nested sub-properties (buffer) - modifies in-place
(tp-remove START END '(PROPERTY SUB-KEY (NESTED-KEYS...)) &optional OBJECT)
;; Remove from entire string - returns NEW string
(tp-remove STRING PROP1 PROP2 ...)
(tp-remove STRING PROPERTY SUB-KEY)
(tp-remove STRING PROPERTY SUB-KEY '(NESTED-KEYS...))
Return Values:
- Buffer forms: Returns
nil - Entire string forms: Returns a new string with properties removed
Examples:
;; Remove entire property
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold help-echo "test"))
(tp-remove 1 10 'face)
(tp-at 1))
;; => (help-echo "test")
;; Remove sub-property from face
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face (:foreground "red" :underline t)))
(tp-remove 1 10 '(face :underline))
(tp-at 1 'face))
;; => (:foreground "red")
;; Remove specific nested keys, keep others
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face (:underline (:style wave :position t :color "blue"))))
(tp-remove 1 10 '(face :underline (:style :position)))
(tp-at 1 '(face :underline)))
;; => (:color "blue") ; :style and :position removed, :color preserved
;; Remove from entire string - returns NEW string, original unchanged
(let ((original (propertize "Hello" 'face 'bold 'help-echo "tip")))
(let ((result (tp-remove original 'face)))
(list :original-face (get-text-property 0 'face original)
:result-face (get-text-property 0 'face result))))
;; => (:original-face bold :result-face nil)
;; Remove sub-property from string - returns NEW string
(let ((original (propertize "Hello" 'face '(:foreground "red" :underline t))))
(let ((result (tp-remove original 'face :underline)))
(list :original (get-text-property 0 'face original)
:result (get-text-property 0 'face result))))
;; => (:original (:foreground "red" :underline t) :result (:foreground "red"))
;; Remove nested keys from string
(let ((original (propertize "Hello" 'face '(:underline (:style wave :color "blue")))))
(let ((result (tp-remove original 'face :underline '(:style))))
(tp-at 0 '(face :underline) result)))
;; => (:color "blue")
tp-clear - Clear All Properties
(tp-clear &optional START END OBJECT)
Clear all text properties from a region. Returns nil.
Examples:
;; Clear region
(with-temp-buffer
(insert "Hello World")
(tp-set 1 10 '(face bold))
(tp-clear 1 10)
(tp-at 1))
;; => nil
;; Clear entire buffer
(with-temp-buffer
(insert "Hello World")
(tp-set 1 12 '(face bold))
(tp-clear)
(tp-at 5))
;; => nil
Pattern Matching Functions
tp-match-set - Match String
(tp-match-set PATTERN PLIST &optional OBJECT START END)
(tp-match-set PATTERN LAYER-NAME &optional OBJECT START END)
Set properties on all occurrences of a string pattern.
PATTERN can be a string (single pattern) or a list of strings (multiple patterns).
PLIST is a property list like '(face bold help-echo "tip").
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
OBJECT is a buffer or string; nil means current buffer.
START and END (new in 0.3.0) restrict matching to the [START, END) portion
of OBJECT, in native coordinates (0-based for strings, 1-based for buffers).
Matching behaves as if OBJECT consisted only of that portion, so no match
crosses the boundaries; reversed bounds are swapped. The same bounds are
accepted by all six tp-match-* / tp-regexp-* functions.
Examples:
;; In buffer - returns list of (START . END) pairs
(with-temp-buffer
(insert "TODO: fix this. TODO: also this.")
(tp-match-set "TODO" '(face warning)))
;; => ((1 . 5) (17 . 21))
;; On string - returns a NEW propertized string (original is not modified)
(tp-match-set "o" '(face bold) "Hello World")
;; => #("Hello World" 4 5 (face bold) 7 8 (face bold))
;; Multiple patterns - match both "world" and "Hello"
(with-temp-buffer
(insert "Hello world, Hello again")
(tp-match-set '("world" "Hello") '(face bold)))
;; => ((7 . 12) (1 . 6) (14 . 19)) ; regions grouped per pattern:
;; "world" first, then each "Hello", in the order patterns are given
;; Multiple patterns on string
(tp-match-set '("Hello" "world") '(face bold) "Hello world")
;; => #("Hello world" 0 5 (face bold) 6 11 (face bold))
;; Use a defined layer name
(define-tp todo-style ()
'(face (:foreground "orange" :weight bold)))
(with-temp-buffer
(insert "TODO: fix this. TODO: also this.")
(tp-match-set "TODO" 'todo-style))
;; => ((1 . 5) (17 . 21))
;; Restrict matching with START/END bounds - only the second TODO is in range
(with-temp-buffer
(insert "TODO one TODO two")
(tp-match-set "TODO" '(face warning) nil 5 18))
;; => ((10 . 14))
tp-match-reset - Match and Reset
Reset (completely replace) all properties on matches.
PATTERN can be a string or list of strings (multiple patterns).
PLIST is a property list like '(face bold help-echo "tip").
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
OBJECT is a buffer or string; nil means current buffer.
(tp-match-reset PATTERN PLIST &optional OBJECT START END)
(tp-match-reset PATTERN LAYER-NAME &optional OBJECT START END)
START and END restrict matching to the [START, END) portion of OBJECT
(see tp-match-set).
Examples:
;; Replaces ALL properties on matched text
(with-temp-buffer
(insert "TODO: fix this")
(tp-set 1 5 '(help-echo "original")) ; Set existing property
(tp-match-reset "TODO" '(face warning))
(tp-at 1))
;; => (face warning) ; help-echo is removed
;; Multiple patterns
(with-temp-buffer
(insert "TODO: fix. FIXME: also fix.")
(tp-match-reset '("TODO" "FIXME") '(face warning)))
;; => ((1 . 5) (12 . 17))
;; Use a defined layer name
(define-tp alert-style ()
'(face (:background "red" :foreground "white")))
(with-temp-buffer
(insert "TODO: fix this")
(tp-match-reset "TODO" 'alert-style))
;; => ((1 . 5))
tp-match-add - Match and Add
Add/merge properties on matches with deep merge support.
PATTERN can be a string or list of strings (multiple patterns).
PLIST is a property list like '(face bold help-echo "tip").
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
OBJECT is a buffer or string; nil means current buffer.
(tp-match-add PATTERN PLIST &optional OBJECT START END)
(tp-match-add PATTERN LAYER-NAME &optional OBJECT START END)
START and END restrict matching to the [START, END) portion of OBJECT
(see tp-match-set).
Examples:
;; Merges with existing properties
(with-temp-buffer
(insert "TODO: fix this")
(tp-set 1 5 '(help-echo "important"))
(tp-match-add "TODO" '(face (:underline t)))
(tp-at 1))
;; => (face (:underline t) help-echo "important")
;; Multiple patterns
(with-temp-buffer
(insert "TODO: fix. FIXME: also fix.")
(tp-match-add '("TODO" "FIXME") '(face (:underline t))))
;; => ((1 . 5) (12 . 17))
;; Use a defined layer name
(define-tp underline-style ()
'(face (:underline (:color "blue" :style wave))))
(with-temp-buffer
(insert "TODO: fix this")
(tp-match-add "TODO" 'underline-style))
;; => ((1 . 5))
tp-regexp-set - Match Regexp
(tp-regexp-set PATTERN PLIST &optional OBJECT START END SUBEXP)
(tp-regexp-set PATTERN LAYER-NAME &optional OBJECT START END SUBEXP)
Set properties on all matches of a regular expression.
PATTERN can be a string (single regexp) or a list of strings (multiple regexps).
PLIST is a property list like '(face bold help-echo "tip").
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
OBJECT is a buffer or string; nil means current buffer.
START and END (new in 0.3.0) restrict matching to the [START, END) portion
of OBJECT, in native coordinates; matching behaves as if OBJECT consisted
only of that portion, and reversed bounds are swapped
(see tp-match-set).
SUBEXP (new in 0.3.0) names a capture group of PATTERN (1 = first group, as
in font-lock highlights): properties apply to that group of each match
instead of the whole match. A match in which the group does not participate
contributes nothing; a SUBEXP beyond the pattern's group count signals a
clear error. All three tp-regexp-* functions accept SUBEXP.
Examples:
;; Highlight all numbers in buffer
(with-temp-buffer
(insert "abc 123 def 456")
(tp-regexp-set "[0-9]+" '(face font-lock-number-face))
(list (tp-at 5 'face) (tp-at 13 'face)))
;; => (font-lock-number-face font-lock-number-face)
;; On string (`case-fold-search' applies by default, so "Hello" matches too;
;; let-bind it to nil for case-sensitive matching)
(tp-regexp-set "[A-Z]+" '(face bold) "Hello WORLD")
;; => #("Hello WORLD" 0 5 (face bold) 6 11 (face bold))
;; Multiple regexps - match both numbers and uppercase letters
;; (with case folding, "abc" matches "[A-Z]+" as well)
(tp-regexp-set '("[0-9]+" "[A-Z]+") '(face bold) "abc 123 XYZ")
;; => #("abc 123 XYZ" 0 3 (face bold) 4 7 (face bold) 8 11 (face bold))
;; Use a defined layer name
(define-tp number-style ()
'(face (:foreground "green")))
(with-temp-buffer
(insert "abc 123 def 456")
(tp-regexp-set "[0-9]+" 'number-style))
;; => ((5 . 8) (13 . 16))
;; SUBEXP - propertize only capture group 1 of each match
(tp-regexp-set "\\([0-9]+\\)px" '(face bold) "margin: 10px 4px" nil nil 1)
;; => #("margin: 10px 4px" 8 10 (face bold) 13 14 (face bold))
;; A match whose group does not participate contributes nothing:
;; "bar" matches the pattern, but group 1 only participates in "foo"
(tp-regexp-set "\\(foo\\)\\|bar" '(face bold) "foo bar" nil nil 1)
;; => #("foo bar" 0 3 (face bold))
;; SUBEXP beyond the pattern's group count signals a clear error
(tp-regexp-set "[0-9]+" '(face bold) "abc 123" nil nil 2)
;; error: Regexp "[0-9]+" has no group 2
;; START/END bounds: as if only that portion existed - the greedy a+
;; matches exactly [1, 3) instead of the whole run
(tp-regexp-set "a+" '(face bold) "aaaa" 1 3)
;; => #("aaaa" 1 3 (face bold))
;; Reversed bounds are swapped
(tp-regexp-set "a+" '(face bold) "aaaa" 3 1)
;; => #("aaaa" 1 3 (face bold))
tp-regexp-reset - Regexp and Reset
Reset (completely replace) all properties on regexp matches.
PATTERN can be a string or list of strings (multiple regexps).
PLIST is a property list like '(face bold help-echo "tip").
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
OBJECT is a buffer or string; nil means current buffer.
(tp-regexp-reset PATTERN PLIST &optional OBJECT START END SUBEXP)
(tp-regexp-reset PATTERN LAYER-NAME &optional OBJECT START END SUBEXP)
START/END bounds and the SUBEXP capture group work exactly as in
tp-regexp-set.
Examples:
;; Reset all properties on regexp matches
(with-temp-buffer
(insert "abc 123 def 456")
(tp-set 5 8 '(help-echo "original"))
(tp-regexp-reset "[0-9]+" '(face bold))
(tp-at 5))
;; => (face bold) ; help-echo is removed
;; On string - returns a NEW string; the original is unchanged
(let ((str (copy-sequence "abc 123 def")))
(tp-set 4 7 '(help-echo "original") str)
(let ((result (tp-regexp-reset "[0-9]+" '(face italic) str)))
(list (tp-at 4 result) (tp-at 4 str))))
;; => ((face italic) (help-echo "original"))
;; Use a defined layer name
(define-tp code-number ()
'(face (:foreground "cyan")))
(with-temp-buffer
(insert "abc 123 def 456")
(tp-regexp-reset "[0-9]+" 'code-number))
;; => ((5 . 8) (13 . 16))
tp-regexp-add - Regexp and Add
Add/merge properties on regexp matches with deep merge support.
PATTERN can be a string or list of strings (multiple regexps).
PLIST is a property list like '(face bold help-echo "tip").
LAYER-NAME can be a symbol representing a layer defined by define-tp or a group defined by define-tps.
OBJECT is a buffer or string; nil means current buffer.
(tp-regexp-add PATTERN PLIST &optional OBJECT START END SUBEXP)
(tp-regexp-add PATTERN LAYER-NAME &optional OBJECT START END SUBEXP)
START/END bounds and the SUBEXP capture group work exactly as in
tp-regexp-set.
Examples:
;; Add properties to regexp matches (preserves existing)
(with-temp-buffer
(insert "abc 123 def 456")
(tp-set 5 8 '(help-echo "number"))
(tp-regexp-add "[0-9]+" '(face bold))
(tp-at 5))
;; => (face bold help-echo "number")
;; On string - returns a NEW string; the original is unchanged
(let ((str (copy-sequence "abc 123 def")))
(tp-set 4 7 '(help-echo "number") str)
(let ((result (tp-regexp-add "[0-9]+" '(face italic) str)))
(list (tp-at 4 result) (tp-at 4 str))))
;; => ((face italic help-echo "number") (help-echo "number"))
;; Use a defined layer name
(define-tp bold-underline ()
'(face (:weight bold :underline t)))
(with-temp-buffer
(insert "abc 123 def 456")
(tp-regexp-add "[0-9]+" 'bold-underline))
;; => ((5 . 8) (13 . 16))
Search & Navigation Functions
tp-search-forward / tp-search-backward
⚠️ Deprecated since 0.3.0. These are raw wrappers for Emacs's
text-property-search-forward/text-property-search-backwardwhose nil-PREDICATE default (match values that are non-nil and notequalto VALUE) contradicts theequal-matching used by the rest of the library. Usetp-forward/tp-backwardfor tp's symmetricequal-matching search — they now expose PREDICATE and NOT-CURRENT too — or call the Emacs primitives directly for raw access. The wrappers keep working, but are marked obsolete (the byte compiler warns on new callers).
(tp-search-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) ; deprecated
(tp-search-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) ; deprecated
tp-forward / tp-backward
(tp-forward PROPERTY &optional VALUE OBJECT N PREDICATE NOT-CURRENT)
(tp-backward PROPERTY &optional VALUE OBJECT N PREDICATE NOT-CURRENT)
Search forward/backward N times for text with PROPERTY.
- N is the number of searches, defaulting to 1.
- VALUE is
equal-matched against directly present property values. Omitting VALUE matches any present value; explicit nil matches a present nil value. Missing-property spans never match. - Pass the unique public sentinel
tp-any-valuewhen wildcard matching is wanted and later positional arguments such as OBJECT or N are supplied. tp-backwardmirrorstp-forward: the same equal-matching semantics, in the opposite direction.- OBJECT can be a buffer or string; nil defaults to current buffer.
- PREDICATE (new in 0.3.0) customizes matching: nil (the default) and t
both keep the 0.2.0
equal-matching contract exactly; a function is called with(VALUE PROP-VALUE)and matches when it returns non-nil. - NOT-CURRENT (new in 0.3.0), when non-nil, skips a matching region
containing point, mirroring the
text-property-search-*primitives. Buffer path only; strings have no point, so it is ignored there. - For buffers, returns the prop-match object from the last successful search.
- For strings, returns a list of (START END VALUE) for the first N runs
where PROPERTY matches, counted from position 0 (point is not involved);
tp-backwardreturns them from end to start.
Examples:
;; Find next text where 'marker equals t
(with-temp-buffer
(insert "Hello World Test")
(tp-set 7 12 '(marker t))
(goto-char 1)
(let ((match (tp-forward 'marker t)))
(when match
(prop-match-beginning match))))
;; => 7
;; Omitting VALUE matches the next directly present marker value
(with-temp-buffer
(insert "Hello World Test")
(tp-set 7 12 '(marker t))
(goto-char 1)
(let ((match (tp-forward 'marker)))
(list (prop-match-beginning match) (prop-match-end match))))
;; => (7 12)
;; Explicit nil matches only a present nil value
(let ((str (copy-sequence "abc")))
(tp-set 1 2 '(marker nil) str)
(tp-forward 'marker nil str))
;; => ((1 2 nil))
;; Backward mirrors forward: same value matching, opposite direction
(with-temp-buffer
(insert "Hello World Test")
(tp-set 7 12 '(marker t))
(goto-char (point-max))
(let ((match (tp-backward 'marker t)))
(list (prop-match-beginning match) (prop-match-end match))))
;; => (7 12)
;; Find next text where 'type equals 'heading
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(type heading))
(goto-char 1)
(let ((match (tp-forward 'type 'heading)))
(when match
(prop-match-value match))))
;; => heading
;; Search in a string
(let ((my-string (copy-sequence "Hello World Hello")))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-forward 'marker tp-any-value my-string 2))
;; => ((0 5 t) (12 17 t))
;; PREDICATE - match with a custom function instead of `equal'
;; (called with VALUE and the region's property value)
(with-temp-buffer
(insert "abcdef")
(tp-set 1 3 '(size 10))
(tp-set 3 6 '(size 20))
(goto-char 1)
(let ((match (tp-forward 'size 15 nil 1
(lambda (target v) (and v (> v target))))))
(list (prop-match-beginning match) (prop-match-end match))))
;; => (3 6) ; the first run whose size exceeds 15
;; PREDICATE works on strings too (returns the first N matching runs)
(let ((str (copy-sequence "hello world")))
(tp-set 0 5 '(size 10) str)
(tp-set 6 11 '(size 20) str)
(tp-forward 'size 15 str 2 (lambda (target v) (and v (> v target)))))
;; => ((6 11 20))
;; NOT-CURRENT - skip the matching region containing point
(with-temp-buffer
(insert "one two")
(tp-set 1 4 '(mark t))
(tp-set 5 8 '(mark t))
(let (a b)
(goto-char 2) ; inside the first mark region
(setq a (prop-match-beginning (tp-forward 'mark t)))
(goto-char 2)
(setq b (prop-match-beginning (tp-forward 'mark t nil 1 nil t)))
(list a b)))
;; => (2 5) ; without NOT-CURRENT the current region matches at point
tp-forward-do / tp-backward-do
(tp-forward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END PREDICATE NOT-CURRENT)
(tp-backward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END PREDICATE NOT-CURRENT)
Search forward/backward TIMES times for text with PROPERTY and apply FUNCTION only at the TIMES-th match.
Despite the -do suffix this is not a for-each — use
tp-search-map to apply
a function to every match.
- FUNCTION receives
(TEXT &optional START END IDX)where TEXT is the matched text, START and END are the positions of the match, and IDX is the 0-based match index. FUNCTION is called with as many of these arguments as it accepts. When FUNCTION returns a string, it replaces the matched text in the string or buffer. - Replacements may change length in buffers (the match is deleted and the replacement inserted). Strings cannot change length in place: a replacement of a different length signals an error; same-length replacements are applied in place.
- PROPERTY is the text property to search for.
- VALUE follows the presence-aware search contract: omitted means any
present value; explicit nil means a present nil value. Use
tp-any-valuewhen later positional arguments are supplied. - OBJECT can be a buffer or string; nil defaults to current buffer.
- TIMES is the number of searches, defaulting to 1. The function searches TIMES times but only applies FUNCTION to the TIMES-th match. All-or-nothing: if fewer than TIMES matches exist, FUNCTION is not applied at all and the number of available matches is returned.
- START and END define the search range; defaults are object start and end.
- PREDICATE and NOT-CURRENT (new in 0.3.0) work as in
tp-forward/tp-backwardand are applied to each underlying search; the defaults keep the 0.2.0 behavior exactly. - Returns the number of successful matches.
Examples:
;; Upcase only the last (2nd) match in string
(let ((my-string (copy-sequence "hello world hello")))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-forward-do #'upcase 'marker tp-any-value my-string 2)
my-string)
;; => "hello world HELLO" ; Only the 2nd match is upcased
;; Search within a range (only matches in range 6-17)
(let ((my-string (copy-sequence "hello world hello")))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-forward-do #'upcase 'marker tp-any-value my-string 2 6 17)
my-string)
;; => "hello world hello" ; only 1 match in range 6-17, so the
;; requested 2nd match does not exist: nothing is applied
;; (all-or-nothing; the call still returns the count, 1)
;; Using function with start and end parameters
;; The function receives position info; use upcase to keep same length
(let ((my-string (copy-sequence "hello world hello"))
(match-info nil))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-forward-do
(lambda (text start end)
(setq match-info (list start end))
(upcase text))
'marker tp-any-value my-string 2)
(list my-string match-info))
;; => ("hello world HELLO" (12 17)) ; Only the last match is transformed
;; Backward search - upcase only the last (2nd) match
(let ((my-string (copy-sequence "hello world hello")))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-backward-do #'upcase 'marker tp-any-value my-string 2)
my-string)
;; => "HELLO world hello" ; The first match (last when searching backward) is upcased
tp-search - Search All Matches
;; Buffer/string region
(tp-search START END PROPERTY &optional VALUE OBJECT)
;; Entire string
(tp-search STRING PROPERTY &optional VALUE)
Search for all text with PROPERTY in a buffer/string range or entire string.
Returns a list of (START END VALUE) for all matching regions.
Examples:
;; Find all 'marker properties in buffer range
(with-temp-buffer
(insert "Hello World Test Again")
(tp-set 1 6 '(marker t))
(tp-set 13 17 '(marker t))
(tp-search 1 22 'marker))
;; => ((1 6 t) (13 17 t))
;; Find all 'type properties with value 'heading in string
(let ((my-string (copy-sequence "Title Here Body Text")))
(tp-set 0 10 '(type heading) my-string)
(tp-search my-string 'type 'heading))
;; => ((0 10 heading))
;; Filter by value
(with-temp-buffer
(insert "Heading1 Body Heading2")
(tp-set 1 9 '(type heading))
(tp-set 10 14 '(type body))
(tp-set 15 23 '(type heading))
(tp-search 1 23 'type 'heading))
;; => ((1 9 heading) (15 23 heading))
tp-search-map - Apply Function to Matched Text
(tp-search-map FUNCTION PROPERTY &optional VALUE OBJECT START END)
Apply FUNCTION to all matches of PROPERTY in OBJECT.
- FUNCTION receives
(TEXT &optional START END IDX)where:- TEXT is the matched text
- START and END are the positions of the match
- IDX is the 0-based index of the current match FUNCTION is called with as many of these arguments as it accepts. When FUNCTION returns a string, it replaces the matched text in the string or buffer.
- Replacements may change length in buffers (the match is deleted and the replacement inserted). Strings cannot change length in place: a replacement of a different length signals an error; same-length replacements are applied in place.
- PROPERTY is the text property to search for.
- VALUE follows the presence-aware search contract: omitted means any
present value; explicit nil means a present nil value. Use
tp-any-valuewhen later positional arguments are supplied. - OBJECT can be a buffer or string; nil defaults to current buffer.
- START and END define the search range; defaults are object start and end.
- Returns the number of matches processed.
Examples:
;; Upcase all markers in string
(let ((my-string (copy-sequence "hello world hello")))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-search-map #'upcase 'marker tp-any-value my-string)
my-string)
;; => "HELLO world HELLO"
;; Search only in a range
(let ((my-string (copy-sequence "hello world hello")))
(tp-set 0 5 '(marker t) my-string)
(tp-set 12 17 '(marker t) my-string)
(tp-search-map #'upcase 'marker tp-any-value my-string 0 10)
my-string)
;; => "HELLO world hello" ; Only first match in range 0-10
;; Custom transformation with start, end, and index
;; The function receives position info; use upcase to keep same length
(let ((my-string (copy-sequence "aaa bbb ccc"))
(positions nil))
(tp-set 0 3 '(marker t) my-string)
(tp-set 4 7 '(marker t) my-string)
(tp-set 8 11 '(marker t) my-string)
(tp-search-map
(lambda (text start end idx)
(push (list idx start end) positions)
(upcase text))
'marker tp-any-value my-string)
(list my-string (nreverse positions)))
;; => ("AAA BBB CCC" ((0 0 3) (1 4 7) (2 8 11)))
;; Custom transformation without optional parameters
(let ((my-string (copy-sequence "hello world")))
(tp-set 0 5 '(marker t) my-string)
(tp-search-map #'upcase 'marker tp-any-value my-string)
my-string)
;; => "HELLO world"
Native Text Property Compatibility
Stage 3 makes the text-property boundary explicit, and Stage 5 adds overlay-aware character-property lookup. These APIs map to GNU Emacs primitives; tp does not manage overlay lifecycle.
tp-lookup-result / tp-lookup
tp-lookup returns a tp-lookup-result record. Use the generated accessors:
| Accessor | Meaning |
|---|---|
tp-lookup-result-property |
requested property |
tp-lookup-result-value |
resolved value |
tp-lookup-result-present-p |
non-nil when the chosen source provides the property; a direct nil counts, while a nil alias follows Emacs' alias fallback |
tp-lookup-result-source |
:text-direct, :category, :alias, :default, or :absent |
tp-lookup-result-mode |
lookup mode |
tp-lookup-result-object |
queried object |
tp-lookup-result-position |
queried position |
tp-lookup-result-overlay |
winning overlay for :char / :char-source, otherwise nil |
Modes:
| Mode | Behavior |
|---|---|
:text-direct |
inspect only direct text properties with text-properties-at; explicit nil is present |
:text-effective |
return get-text-property's effective text value and report its text source |
:text-source |
report the winning text source without overlays |
:char |
overlay-aware get-char-property-and-overlay value; text fallback follows Emacs |
:char-source |
like :char, plus :overlay source and overlay identity when an overlay wins |
;; Direct lookup distinguishes explicit nil from absence
(let ((str (copy-sequence "ab")))
(put-text-property 0 1 'state nil str)
(let ((nil-result (tp-lookup 0 'state :object str :mode :text-direct))
(absent-result (tp-lookup 1 'state :object str :mode :text-direct)))
(list (list (tp-lookup-result-present-p nil-result)
(tp-lookup-result-value nil-result)
(tp-lookup-result-source nil-result))
(list (tp-lookup-result-present-p absent-result)
(tp-lookup-result-value absent-result)
(tp-lookup-result-source absent-result)))))
;; => ((t nil :text-direct) (nil nil :absent))
;; Source-aware lookup explains category/default/alias/direct text sources
(let* ((str (copy-sequence "a"))
(category (make-symbol "tp-doc-category")))
(put category 'state 'category-value)
(put-text-property 0 1 'category category str)
(let ((result (tp-lookup 0 'state :object str :mode :text-source)))
(list (tp-lookup-result-value result)
(tp-lookup-result-source result))))
;; => (category-value :category)
;; Character-source lookup reports the winning overlay identity
(with-temp-buffer
(insert "x")
(let ((low (make-overlay 1 2))
(high (make-overlay 1 2)))
(overlay-put low 'priority 1)
(overlay-put low 'state 'low)
(overlay-put high 'priority 10)
(overlay-put high 'state 'high)
(let ((result (tp-lookup 1 'state :mode :char-source)))
(list (tp-lookup-result-value result)
(tp-lookup-result-source result)
(eq (tp-lookup-result-overlay result) high)))))
;; => (high :overlay t)
tp-lookup reports overlay winners, but overlay creation, deletion, movement, priority management, and lifecycle remain native Emacs responsibilities.
tp-property-change
tp-property-change wraps next-property-change, previous-property-change, next-single-property-change, and previous-single-property-change.
(tp-property-change POSITION :object OBJECT :limit LIMIT :direction :next)
(tp-property-change POSITION :property PROPERTY :object OBJECT :limit LIMIT :direction :previous)
Omit :property for any property change. Pass :property for a single-property change. :direction is :next or :previous.
tp-property-any / tp-property-not-all
These are thin wrappers over Emacs primitives:
(tp-property-any START END PROPERTY VALUE &optional OBJECT)
(tp-property-not-all START END PROPERTY VALUE &optional OBJECT)
They preserve Emacs behavior exactly, including explicit nil matching.
tp-with-mutation-policy
tp-with-mutation-policy makes modified/read-only behavior explicit for buffer mutations:
| Policy | Behavior |
|---|---|
(:modified :ordinary :read-only :respect) |
normal Emacs mutation; read-only text can signal |
(:modified :ordinary :read-only :inhibit) |
bind inhibit-read-only and record ordinary modified/undo state |
(:modified :silent :read-only :inhibit) |
bind inhibit-read-only and use with-silent-modifications |
(:modified :silent :read-only :respect) is rejected because silent modification cannot be combined with respecting read-only text.
Insert/copy/yank/stickiness/narrowing/indirect-buffer behavior is delegated directly to Emacs. tp does not provide wrappers for those operations; use the native primitives (insert, insert-and-inherit, copy-sequence, substring, insert-for-yank, narrowing commands, and indirect buffers).
The Property Layer System
The property layer system is tp.el's innovative feature that allows stacking multiple sets of properties on the same text region. Only the top layer is visible, but lower layers are preserved and can be revealed through rotation or pinning.
Custom Text Properties
Custom text properties is a general-purpose feature provided by tp.el. After defining with define-tp, they can be set using core functions like tp-set/tp-reset/tp-add.
Core Features
-
Mixed Use with Built-in Properties: Custom text properties can be seamlessly mixed with built-in Emacs text properties (such as
face,display,help-echo, etc.). -
Automatic Merging of Duplicate Properties: In a single setting operation, if the same property (e.g.,
face) is specified multiple times, they are automatically merged rather than simply overwritten.
;; Define a custom text property
(define-tp tp-highlight ()
'(face (:background "yellow")))
;; Mixed use with built-in properties
(tp-set 1 10 '(tp-highlight t face bold help-echo "tip"))
;; Result: Has tp-highlight's background color, bold style, and help-echo property
;; Automatic merging of duplicate properties example
(tp-set "emacs"
'face 'bold
'face '(:background "green")
'face '(:foreground "red"))
;; Result: face is ((:foreground "red") (:background "green") bold)
;; Three face properties stack into one face list, most recent first
;; Later values override earlier ones for the same sub-property
(tp-set "emacs"
'face '(:foreground "red")
'face '(:foreground "yellow"))
;; Result: foreground is "yellow"
;; Use with tp-palette layer
(tp-set "emacs"
'tp-palette 'info
'face '(:foreground "red"))
;; Result: tp-palette's face is merged with (:foreground "red")
Custom Text Property Groups
Using define-tps, you can define multiple related text property groups that can be used individually or as a group.
Text Property Layers
Text property layers is a unique feature of tp.el that requires specific functions (tp-put-layer/tp-push-layer) to set and use.
Core Features
-
Layer-Related Properties: When set using
tp-push-layer/tp-put-layer, layer-related properties (tp-name,tp-layers) are automatically introduced to support layer stacking and operations. -
Layer Stacking Mechanism: Multiple sets of properties can be stacked on the same text region, with only the top layer visible while lower layers are preserved.
-
Rich Layer Operations: Supports various layer operations such as rotation, deletion, merging, etc.
;; Define a text property (can be used as custom property or layer)
(define-tp tp-highlight ()
'(face (:background "yellow")))
;; Use as regular custom text property (no layer properties)
(tp-set 1 10 '(tp-highlight t))
;; Result: Only face property, no tp-name
;; Use as text property layer (introduces layer-related properties)
(tp-push-layer 1 10 'tp-highlight)
;; Result: Both face and tp-name properties, supports layer operations
When to Use Which
| Scenario | Recommended Method | Description |
|---|---|---|
| Simple property setting | tp-set/tp-reset/tp-add |
When you only need to set text properties without layer stacking |
| Mixed with built-in properties | tp-set/tp-reset/tp-add |
Custom properties can be seamlessly mixed with built-in properties |
| Need layer stacking | tp-push-layer/tp-put-layer |
When you need to stack multiple sets of properties on the same text region |
| Need layer operations | tp-push-layer/tp-put-layer |
When you need to perform rotation, deletion, and other layer operations |
Property Layer Concept
┌─────────────────────────────┐
│ TOP LAYER (visible) │ ← idx=0, What you see
├─────────────────────────────┤
│ Middle Layer (hidden) │ ← idx=1, Preserved
├─────────────────────────────┤
│ Bottom Layer (hidden) │ ← idx=-1, Preserved
└─────────────────────────────┘
Property Layer Definition
define-tp / define-tps - Define Custom Text Properties
Since 0.3.0 the prefix-conforming aliases
tp-define-layer(fordefine-tp),tp-define-group(fordefine-tps) andtp-define-palette(fordefine-tp-palette) are the canonical names going forward — they make the macros discoverable viaC-h f tp-.... The historical names are permanent aliases and will never be removed; this README's examples keep using them.
define-tp - Define Single Custom Text Property (Layer)
Define a custom text property. The name does not need to be quoted. The ARGLIST is mandatory in every format: () for non-parameterized layers (including the reactive keyword format), (ARG1 ARG2 ...) with any number of parameter symbols for parameterized layers. Supports three formats:
Format 1 - Non-parameterized (empty argument list, simple properties):
(define-tp tp-bold ()
'(face bold))
;; Usage:
(tp-set "emacs" 'tp-bold t)
(tp-set 0 5 '(tp-bold t) "emacs")
Format 2 - Parameterized (with one or more arguments):
(define-tp tp-space (pixel)
`(display (space :width (,pixel))))
;; Usage:
(tp-set "emacs" 'tp-space 2)
(tp-set 0 5 '(tp-space 2) "emacs")
Since 0.3.0 the arglist may declare any number of parameters. The call
specs accept the arguments flat — (LAYER ARG1 ... ARGN) — or wrapped in
one list — (LAYER (ARG1 ... ARGN)) — and both work in tp-set and
tp-put-layer:
(define-tp tp-colors (fg bg)
`(face (:foreground ,fg :background ,bg)))
;; Whole-string form: arguments follow the layer name
(tp-set "hello" 'tp-colors "red" "blue")
;; => #("hello" 0 5 (face (:foreground "red" :background "blue")))
;; Region form, wrapped argument list plus extra properties
(let ((str (copy-sequence "hello")))
(tp-set 0 5 '(tp-colors ("red" "blue") help-echo "tip") str)
(list (tp-at 0 'face str) (tp-at 0 'help-echo str)))
;; => ((:foreground "red" :background "blue") "tip")
;; tp-put-layer spec
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 '(tp-colors "white" "black") 0)
(tp-at 1 'face))
;; => (:foreground "white" :background "black")
;; Wrong-arity calls signal a clear error naming the layer and both counts
(tp-set "hello" 'tp-colors "red")
;; error: tp layer tp-colors takes 2 argument(s), got 1
Parameterized groups (define-tps) accept multiple parameters the same
way; the (GROUP ARG1 ... ARGN) and (GROUP (ARG1 ... ARGN)) specs work
in the tp-set family. Note: $-symbols in parameterized bodies resolve
to their variables' current values at expansion time — parameterized
layers are not reactive.
Format 3 - With reactive features (:props, :data, :compute, :watch, :transform):
(define-tp my-reactive-layer ()
:props '(face (:foreground $my-color) help-echo $status-note)
:data '((my-color . "red") (status . "active"))
:compute '((status-note (lambda () (concat "status: " status))))
:watch '((my-color (lambda (new old layer) (message "Color changed!"))))
:transform (lambda (text) (upcase text)))
;; Usage:
(tp-push-layer 1 10 'my-reactive-layer)
;; Changing the variable automatically updates the text
(setq my-color "blue")
Reactive Keywords:
- :props - Property list where
$-prefixed symbols are reactive variables - :data - Additional reactive variables (can include initial values)
- :compute - Computed properties that derive values from other variables
- :watch - Watchers that execute callbacks when variables change
- :transform - Transform function to process
tp-textvalues before display
Note: the values of :props, :data, :compute, and :watch must be
quoted (they are evaluated when the layer is defined); :transform
takes a function.
define-tps - Define Custom Text Property Group (Layer Group)
Define multiple related custom text properties. The name does not need to be quoted. As with define-tp, the ARGLIST is mandatory: () for non-parameterized groups, (ARG1 ARG2 ...) for parameterized ones (any number of parameters since 0.3.0). Properties in the group can be used individually or with the group name to set multiple layers.
Format 1 - Non-parameterized (empty argument list):
(define-tps tp-moon-phases ()
'(display "🌑")
'(display "🌕"))
;; Usage:
(tp-set 1 6 'tp-moon-phases)
Format 2 - Parameterized (with single argument):
;; First define parameterized individual layers
(define-tp tp-color1 (color)
`(face (:foreground ,color)))
(define-tp tp-color2 (color)
`(face (:foreground ,color)))
(define-tp tp-bg ()
'(face (:background "green")))
;; Define parameterized layer group referencing the layers above
(define-tps tp-themed-status (color)
`(tp-color1 ,color) ;; Use group parameter
'(tp-color2 "red") ;; Use fixed parameter
'tp-bg) ;; Reference non-parameterized layer
;; Usage - sets multi-layer properties:
(tp-set "emacs" 'tp-themed-status "orange")
;; Result: Three layers stacked, tp-color1 is top layer with "orange" color
Supported layer definition formats within the group:
-
Anonymous layers (named as NAME-0, NAME-1, etc.):
'(face (:background "yellow")) -
Named layers with cons-cell (named as NAME-suffix):
'("highlight" . (face (:background "yellow"))) -
Named layers with :props keyword:
'("highlight" :props (face (:background "yellow"))) -
Named layers with reactive features (:props, :data, :watch, :compute):
'("reactive" :props (face (:foreground $my-color)) :data ((my-color . "red")) :watch ((my-color (lambda (new old layer) (message "Changed!")))))
Examples:
;; Define non-parameterized custom text property
(define-tp tp-highlight ()
'(face (:background "yellow")))
;; Define parameterized custom text property
(define-tp tp-color (color)
`(face (:foreground ,color)))
;; Define property group
(define-tps tp-status ()
'("success" . (face (:foreground "green")))
'("warning" . (face (:foreground "orange")))
'("error" . (face (:foreground "red"))))
;; Use custom text properties
(tp-set "Hello" 'tp-highlight t) ; Non-parameterized
(tp-set "Hello" 'tp-color "blue") ; Parameterized
(tp-set 1 6 'tp-status) ; Use layer group
;; Use as layers (supports stacking operations)
(tp-push-layer 1 10 'tp-highlight)
The first layer in the definition is the top layer (visible by default).
More Examples:
;; Define status layers, then group them
(progn
(tp-layer-reset)
(define-tp highlight ()
'(face (:background "yellow" :foreground "black")))
(define-tp error ()
'(face (:background "red" :foreground "white")))
(define-tp info ()
'(face (:background "blue" :foreground "white")))
(define-tps status-colors ()
'highlight 'error 'info)
(length (tp-group-props 'status-colors)))
;; => 3
;; Define a layer group with named layers
(progn
(tp-layer-reset)
(define-tps moon-phases ()
'("new" . (display "🌑"))
'("waxing-crescent" . (display "🌒"))
'("first-quarter" . (display "🌓"))
'("full" . (display "🌕")))
(tp-layer-props 'moon-phases-full))
;; => (display "🌕")
;; Parameterized layer group referencing other defined layers
(progn
(tp-layer-reset)
(define-tp tp-test-l1 (color)
`(face (:foreground ,color)))
(define-tp tp-test-l2 (color)
`(face (:foreground ,color)))
(define-tp tp-test-l3 ()
'(face (:background "green")))
(define-tps tp-test-group1 (color)
`(tp-test-l1 ,color) ;; Use group parameter
'(tp-test-l2 "red") ;; Use fixed parameter
'tp-test-l3) ;; Reference non-parameterized layer
(tp-set "emacs" 'tp-test-group1 "orange"))
;; => #("emacs" 0 5 (face (:foreground "orange") tp-name tp-test-l1
;; tp-layers ((face (:foreground "red") tp-name tp-test-l2)
;; (face (:background "green") tp-name tp-test-l3))))
;; (top-level property print order may differ across Emacs versions;
;; the tp-layers stack order itself is stable)
tp-layer-props / tp-group-props
(tp-layer-props LAYER-NAME &optional INCLUDE-TP-NAME)
(tp-group-props GROUP-NAME &optional INCLUDE-TP-NAME)
Get properties for a layer or all layers in a group.
By default the result contains only the layer's own properties. When
INCLUDE-TP-NAME is non-nil, a tp-name LAYER-NAME entry is appended
(the form used internally by the layer stack). Exception: layers with
registered reactive dependencies always include tp-name — the
reactive engine uses it to locate and re-render their regions.
Examples:
;; Get layer properties (no tp-name by default)
(progn
(tp-layer-reset)
(define-tp my-layer ()
'(face bold help-echo "tip"))
(list (tp-layer-props 'my-layer)
(tp-layer-props 'my-layer t)))
;; => ((face bold help-echo "tip")
;; (face bold help-echo "tip" tp-name my-layer))
;; Get group properties
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(define-tps my-group ()
'layer1 'layer2)
(length (tp-group-props 'my-group)))
;; => 2
tp-layer-props-with-args / tp-group-props-with-args / tp-layer-arglist
(tp-layer-props-with-args LAYER-NAME ARGS &optional INCLUDE-TP-NAME)
(tp-group-props-with-args GROUP-NAME ARGS &optional INCLUDE-TP-NAME)
(tp-layer-arglist LAYER-NAME)
Introspection for parameterized layers and groups (new in 0.3.0):
tp-layer-props-with-argsexpands a parameterized layer with ARGS, a list of values bound positionally to the layer's parameters. Extra values are ignored; fewer values than parameters signal a wrong-arity error. Returns a fresh copy (mutating it cannot corrupt the registry), or nil for non-parameterized or undefined layers. The existing single-argumenttp-layer-props-with-arg(note the one-character name difference) remains as a thin(list ARG)wrapper.tp-group-props-with-argsis the group counterpart, returning the list of expanded per-layer plists;tp-group-props-with-argremains as the single-argument convenience.tp-layer-arglistreturns a copy of the layer's parameter list, or nil when LAYER-NAME is not a parameterized layer.
Examples:
(progn
(tp-layer-reset)
(define-tp tp-colors (fg bg)
`(face (:foreground ,fg :background ,bg)))
(tp-layer-props-with-args 'tp-colors '("red" "blue")))
;; => (face (:foreground "red" :background "blue"))
;; The parameter list itself
(tp-layer-arglist 'tp-colors)
;; => (fg bg)
;; Groups expand to one plist per layer
(progn
(define-tps tp-badge (fg bg)
`(tp-colors ,fg ,bg)
'(face bold))
(tp-group-props-with-args 'tp-badge '("white" "black")))
;; => ((face (:foreground "white" :background "black")) (face bold))
;; Too few arguments signal the same clear arity error as tp-set
(tp-layer-props-with-args 'tp-colors '("red"))
;; error: tp layer tp-colors takes 2 argument(s), got 1
tp-describe-layer - Describe a Layer
(tp-describe-layer NAME) ; interactive
Pop a help buffer describing layer NAME (with completion over all registered layers when called interactively). The buffer shows the storage format (flat / unified / parameterized / reactive), the raw stored body, the expanded properties (or a placeholder for parameterized layers, which need arguments), the parameter list, the reactive variables the layer depends on, whether a transform is registered, and the group that generated the layer, if any.
(progn
(tp-layer-reset)
(define-tp tp-colors (fg bg)
`(face (:foreground ,fg :background ,bg)))
(tp-describe-layer 'tp-colors))
;; Pops a *Help* buffer:
;; tp-colors is a tp layer.
;;
;; Storage format: parameterized
;; Arguments: (fg bg)
;; Stored body: `(face (:foreground ,fg :background ,bg))
;; Expanded props: parameterized layer: expand with `tp-layer-props-with-args'
;; Reactive deps: none
;; Transform: no
tp-undefine-layer / tp-undefine-group
(tp-undefine-layer NAME)
(tp-undefine-group NAME)
Remove layer or group definition.
Examples:
;; Undefine a layer
(progn
(tp-layer-reset)
(define-tp temp-layer () '(face bold))
(tp-undefine-layer 'temp-layer)
(tp-layer-props 'temp-layer))
;; => nil
;; Undefine a group
(progn
(tp-layer-reset)
(define-tp l1 () '(face bold))
(define-tps my-group ()
'l1)
(tp-undefine-group 'my-group)
(assoc 'my-group tp-layer-groups))
;; => nil
tp-layer-reset
(tp-layer-reset)
Clear all layer and group definitions, including all reactive dependencies and watchers.
Examples:
(progn
(define-tp test-layer () '(face bold))
(tp-layer-reset)
(list tp-layer-alist tp-layer-groups))
;; => (nil nil)
tp-reactive-reset
(tp-reactive-reset)
Clear all reactive text property watchers and dependencies, without affecting layer definitions.
This is useful when you want to remove all reactive bindings but keep the layer definitions intact.
Examples:
;; Define a reactive layer
(progn
(defvar my-reactive-color "red")
(define-tp reactive-layer ()
:props '(face (:foreground $my-reactive-color)))
;; Clear reactive bindings only
(tp-reactive-reset)
;; Layer still exists, but changing my-reactive-color no longer updates it
(tp-layer-props 'reactive-layer))
;; => (face (:foreground "red"))
Property Layer Placement
⚠️ String forms of stack operations mutate in place. Unlike
tp-set, which returns a new propertized string, the string form of every stack mutator (tp-put-layer,tp-push-layer,tp-pop-layer,tp-delete-layer,tp-move-layer,tp-raise-layer,tp-lower-layer,tp-rotate-layer,tp-pin-layer,tp-switch-layer,tp-hide-layer,tp-show-layer,tp-merge-layers,tp-flatten-layers,tp-add-to-layers,tp-add-to-all-layers) modifies STRING destructively. Never pass a string literal or a shared string you do not own — usecopy-sequencefirst. Unifying this withtp-set's copy semantics is on the 0.4 ledger.
Return values (0.3.0): tp-put-layer / tp-push-layer return OBJECT
when one was given (the string itself in string forms), else
(START . END). Every other stack mutator returns the number of property
runs it modified; a missing layer name or index never signals — unmatched
runs are silently left alone, and a return value of 0 means nothing matched.
tp-put-layer - Set Layer at Index
;; Buffer/string region
(tp-put-layer START END LAYER IDX OBJECT NOERROR)
;; Entire string
(tp-put-layer STRING LAYER IDX NOERROR)
Set layer(s) at a specific index position in the layer stack.
IDX = 0: Top (visible layer)IDX = -1: Bottom- Other values insert at that position
LAYER accepts several specs:
- a layer name defined with
define-tp:'highlight - an inline property plist (no
define-tpneeded):'(face bold help-echo "tip") - a list of layer names (the first name ends up on top):
'(layer-a layer-b) - a parameterized layer call:
'(tp-color "red")— multi-argument layers work too:'(tp-colors "white" "black")
Stack model: only the top layer's properties are the visible text
properties; lower layers are stored in the tp-layers text property until
they are raised, rotated, or flattened.
NOERROR (new in 0.3.0): a LAYER naming an undefined layer or group
normally signals an error. With NOERROR non-nil the call returns nil
instead and modifies nothing — handy when applying layers that may not be
defined yet. tp-push-layer accepts the same trailing NOERROR.
Examples:
;; Put base layer at top
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 'base 0)
(tp-at 1 'tp-name)))
;; => base
;; Put highlight at index 1 (below top)
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 'base 0)
(tp-put-layer 1 10 'highlight 1)
(tp-layer-count 1 10)))
;; => 2
;; Put layer at bottom
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp info () '(face (:foreground "blue")))
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 'base 0)
(tp-put-layer 1 10 'info -1)
(tp-layer-top 1 10)))
;; => base ; info is at bottom, base is visible
;; Inline plist - no define-tp needed
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 '(face bold help-echo "tip") 0)
(list (tp-at 1 'face) (tp-at 1 'help-echo)))
;; => (bold "tip")
;; List of layer names - layer-a ends up on top
(progn
(tp-layer-reset)
(define-tp layer-a () '(face bold))
(define-tp layer-b () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 '(layer-a layer-b) 0)
(list (tp-at 1 'face) (tp-layer-list 1 10))))
;; => (bold (layer-a layer-b))
;; Parameterized layer call
(progn
(tp-layer-reset)
(define-tp tp-color (color)
`(face (:foreground ,color)))
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 '(tp-color "red") 0)
(tp-at 1 'face)))
;; => (:foreground "red")
;; NOERROR - an undefined layer name returns nil instead of signaling
(with-temp-buffer
(insert "Hello World")
(tp-put-layer 1 10 'no-such-layer 0 nil t))
;; => nil ; nothing modified
tp-push-layer - Push Layer to Top
;; Buffer/string region
(tp-push-layer START END LAYER OBJECT NOERROR)
;; Entire string
(tp-push-layer STRING LAYER NOERROR)
Push a layer to the top of the stack (equivalent to tp-put-layer ... 0).
NOERROR (new in 0.3.0) works as in
tp-put-layer: an undefined LAYER
returns nil instead of signaling.
Examples:
;; Push base layer first
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-at 1 'tp-name)))
;; => base
;; Push highlight on top (now visible)
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-at 1 'tp-name)))
;; => highlight
;; The top layer's props are visible; lower layers wait in `tp-layers'
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(list :face (tp-at 1 'face)
:top (tp-layer-top 1 10)
:layers (tp-layer-list 1 10)
:hidden (length (tp-at 1 'tp-layers)))))
;; => (:face (:background "yellow") :top highlight :layers (highlight base) :hidden 2)
Property Layer Deletion
tp-delete-layer - Delete Layer by Name/Index
;; Buffer/string region
(tp-delete-layer START END LAYER-NAME/IDX OBJECT)
;; Entire string
(tp-delete-layer STRING LAYER-NAME/IDX)
Delete a layer from anywhere in the stack by name or index.
Examples:
;; Remove by name
(progn
(tp-layer-reset)
(define-tp highlight () '(face (:background "yellow")))
(define-tp base () '(face default))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-delete-layer 1 10 'highlight)
(tp-at 1 'tp-name)))
;; => base
;; Remove top layer (idx=0)
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-delete-layer 1 10 0)
(tp-at 1 'tp-name)))
;; => layer1
;; Remove bottom layer
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-delete-layer 1 10 -1)
(tp-layer-count 1 10)))
;; => 1
tp-pop-layer - Pop Top Layer
;; Buffer/string region
(tp-pop-layer START END OBJECT)
;; Entire string
(tp-pop-layer STRING)
Remove the top layer (equivalent to tp-delete-layer ... 0).
Examples:
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-pop-layer 1 10)
(tp-at 1 'tp-name)))
;; => layer1
Property Layer Movement
tp-move-layer - Move Layer to Position
;; Buffer/string region
(tp-move-layer START END FROM-ID TO-IDX OBJECT)
;; Entire string
(tp-move-layer STRING FROM-ID TO-IDX)
Move a layer from one position to another in the layer stack.
FROM-IDidentifies the layer to move: an integer index or a layer name symbolTO-IDXis the target position (integer index)- Index 0 means top (visible), -1 means bottom
- Both indices refer to positions before the move
This is the generic layer movement function used internally by tp-raise-layer, tp-rotate-layer, tp-pin-layer, and tp-switch-layer.
Examples:
;; Move layer at index 2 to index 0 (top)
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(define-tp layer3 () '(face underline))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-push-layer 1 10 'layer3)
;; Stack: layer3 (0), layer2 (1), layer1 (2)
(tp-move-layer 1 10 2 0)
(tp-layer-top 1 10)))
;; => layer1
;; Move layer by name to bottom
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
;; Stack: layer2 (top), layer1 (bottom)
(tp-move-layer 1 10 'layer2 -1)
(tp-layer-top 1 10)))
;; => layer1
;; Move on string
(let ((str (copy-sequence "Hello")))
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(tp-push-layer str 'layer1)
(tp-push-layer str 'layer2)
;; layer2 is on top
(tp-move-layer str 'layer1 0)
(tp-at 0 'tp-name str))
;; => layer1
tp-raise-layer - Move Layer Up/Down
;; Buffer/string region
(tp-raise-layer START END IDX/LAYER-NAME N OBJECT)
;; Entire string
(tp-raise-layer STRING IDX/LAYER-NAME N)
Raise a layer by N positions. Positive N moves toward top, negative moves toward bottom.
Examples:
;; Move layer1 up by 2 positions (to top)
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(define-tp layer3 () '(face underline))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-push-layer 1 10 'layer3)
;; Stack: layer3 (top), layer2, layer1 (bottom)
(tp-raise-layer 1 10 'layer1 2)
(tp-layer-top 1 10)))
;; => layer1
;; Move layer at idx 0 down by 1 position
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
;; Stack: layer2 (idx 0), layer1 (idx 1)
(tp-raise-layer 1 10 0 -1)
(tp-layer-top 1 10)))
;; => layer1
tp-lower-layer - Mirror of tp-raise-layer
;; Buffer/string region
(tp-lower-layer START END IDX/LAYER-NAME N OBJECT)
;; Entire string
(tp-lower-layer STRING IDX/LAYER-NAME N)
Lower a layer by N positions (new in 0.3.0). The mirror image of
tp-raise-layer: positive N moves the layer down toward the bottom,
negative N moves it up. N defaults to 1, and the resulting position is
clamped to the stack. Returns the number of property runs modified.
Examples:
;; Lower the top layer by one position
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(define-tp layer3 () '(face underline))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-push-layer 1 10 'layer3)
;; Stack: layer3 (top), layer2, layer1 (bottom)
(tp-lower-layer 1 10 'layer3 1)
;; Stack: layer2 (top), layer3, layer1 (bottom)
(list (tp-layer-top 1 10) (tp-layer-list 1 10))))
;; => (layer2 (layer2 layer3 layer1))
tp-rotate-layer - Cycle Layers
;; Buffer/string region (canonical order, OBJECT last - new in 0.3.0)
(tp-rotate-layer START END DIRECTION &optional COUNT OBJECT)
;; Entire string
(tp-rotate-layer STRING DIRECTION COUNT)
;; Buffer/string region (legacy order, kept working forever)
(tp-rotate-layer START END OBJECT)
Rotate layers by COUNT steps, preserving their relative order.
- DIRECTION is
downor nil to move the top layer to the bottom (the historical behavior), orupto bring the bottom layer to the top; any other value signals an error. - COUNT is the number of rotation steps, defaulting to 1; a COUNT below 1 rotates nothing. Hidden layers rotate with the rest of the stack.
- Returns the number of property runs modified.
The two region orders are told apart by the third argument: the symbols
up / down are never valid OBJECTs, so (tp-rotate-layer 1 5 'up)
unambiguously selects the canonical (START END DIRECTION [COUNT] [OBJECT]) order — no nil OBJECT placeholder needed. Any other third
argument (a buffer, a string, or nil for the current buffer) selects the
legacy (START END OBJECT [DIRECTION] [COUNT]) order, which keeps working.
Examples:
;; Stack: highlight (top) -> base (bottom)
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
;; Stack: highlight (top) -> base (bottom)
(tp-rotate-layer 1 10)
;; Stack: base (top) -> highlight (bottom)
(tp-layer-top 1 10)))
;; => base
;; Canonical order: `up' brings the bottom layer to the top
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(define-tp layer3 () '(face underline))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-push-layer 1 10 'layer3)
;; Stack: layer3 (top), layer2, layer1 (bottom)
(tp-rotate-layer 1 10 'up)
(tp-layer-list 1 10)))
;; => (layer1 layer3 layer2)
;; COUNT rotates several steps at once
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(define-tp layer3 () '(face underline))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-push-layer 1 10 'layer3)
(tp-rotate-layer 1 10 'down 2)
(tp-layer-list 1 10)))
;; => (layer1 layer3 layer2)
tp-pin-layer - Pin Layer to Top
;; Buffer/string region
(tp-pin-layer START END IDX/LAYER-NAME OBJECT)
;; Entire string
(tp-pin-layer STRING IDX/LAYER-NAME)
Move a layer to the top of the stack. One-shot: despite the name,
nothing stays pinned — this is a single move to index 0, and nothing
prevents a later tp-push-layer or tp-put-layer from covering the moved
layer again.
Examples:
;; Make 'base the top layer
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
;; highlight is on top
(tp-pin-layer 1 10 'base)
(tp-layer-top 1 10)))
;; => base
tp-switch-layer - Switch Two Layers
;; Buffer/string region
(tp-switch-layer START END IDX1/NAME1 IDX2/NAME2 OBJECT)
;; Entire string
(tp-switch-layer STRING IDX1/NAME1 IDX2/NAME2)
Swap positions of two layers.
Examples:
;; Switch layer1 and layer2
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
;; layer2 is on top
(tp-switch-layer 1 10 'layer1 'layer2)
;; Now layer1 is on top
(tp-layer-top 1 10)))
;; => layer1
Property Layer Visibility
tp-hide-layer / tp-show-layer - Hide and Show Layers
;; Buffer/string region
(tp-hide-layer START END NAME OBJECT)
(tp-show-layer START END NAME OBJECT)
;; Entire string
(tp-hide-layer STRING NAME)
(tp-show-layer STRING NAME)
Hide a layer without removing it, and make it render again (new in 0.3.0). NAME identifies the layer: a layer name symbol or an integer index into the full stack, hidden layers included (0 = top, -1 = bottom).
The visibility model:
- A hidden layer stays in the stack: it still counts for
tp-layer-count, appears intp-layer-listandtp-layer-stack-at, and can be moved, raised, or lowered — but it does not render. The text shows the properties of the topmost non-hidden layer instead. - Hiding the currently visible top layer therefore reveals the next visible layer below it.
- When every layer is hidden the text renders bare (only the
tp-layersbookkeeping property remains — not eventp-namerenders) while all layers stay queryable. - A hidden layer keeps receiving reactive updates while hidden, so
tp-show-layeralways reveals current values (see Layer-Buffer Registry & Lifecycle). tp-flatten-layersmerges only visible layers, andtp-merge-layersexcludes hidden matched layers' properties — hiding can never leak (see Property Layer Merging).- Hiddenness is stored as a
tp-hiddenflag inside the layer's plist intp-layersstack storage, sotp-hiddenis a reserved property name inside layers, liketp-name. - While any layer is hidden, direct properties are a render cache for the
first visible managed layer. Definition/reactive refresh has enough
ownership context to preserve native edits on that visible layer. A normal
stack decode/write remains strict and signals
tp-layer-conflictbefore changing state when the cache differs; properties appearing while every layer is hidden are always a conflict.
Both functions return the number of property runs modified. A NAME matching no layer never signals, and hiding an already-hidden layer (or showing a visible one) is a silent no-op — 0 means nothing changed.
Examples:
;; Hiding the top layer reveals the one below; the stack is intact
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-hide-layer 1 10 'highlight)
(list :visible (tp-at 1 'tp-name)
:face (tp-at 1 'face)
:count (tp-layer-count 1 10)
:layers (tp-layer-list 1 10))))
;; => (:visible base :face default :count 2 :layers (highlight base))
;; With every layer hidden the text renders bare
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-hide-layer 1 10 'highlight)
(tp-hide-layer 1 10 'base)
(list :face (tp-at 1 'face) :count (tp-layer-count 1 10))))
;; => (:face nil :count 2)
;; tp-show-layer restores the layer's rendering
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-hide-layer 1 10 'highlight)
(tp-show-layer 1 10 'highlight)
(tp-at 1 'face)))
;; => (:background "yellow")
;; Return value: number of modified runs; a missing name is a silent 0
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(list (tp-hide-layer 1 10 'base)
(tp-hide-layer 1 10 'base) ; already hidden
(tp-hide-layer 1 10 'nonexistent)))) ; no such layer
;; => (1 0 0)
Managed Layer Lifecycle
Stage 4 adds explicit lifecycle metadata for managed layer stacks. A single managed layer now uses tp-layers storage when metadata is present, because tp-meta is authoritative lifecycle state. The rendered direct text properties and public stack queries strip tp-meta: text-properties-at should show only rendered properties plus stack storage, and tp-layer-stack-at returns public layer plists without metadata.
Legacy return values remain unchanged. Stack mutators still return their documented object/range/run-count values; diagnostics are separate read-only APIs.
Parameterized mounted layers store their args and definition version in tp-meta. Redefining a parameterized layer refreshes mounted entries from stored args. Entries created before metadata existed are treated conservatively as legacy entries.
tp-attach-managed-layers / tp-detach-managed-layers
(tp-attach-managed-layers START END &optional OBJECT)
(tp-detach-managed-layers START END &optional OBJECT KEEP-RENDERED)
tp-attach-managed-layers scans a range that already contains managed layer storage, normalizes missing metadata, registers found layers in the reactive buffer registry, and returns discovered layer names in range order. Use it after inserting a propertized managed string through native insertion paths.
tp-detach-managed-layers removes managed storage and returns detached layer names. When KEEP-RENDERED is non-nil, the currently visible rendered properties remain as ordinary text properties; lifecycle storage (tp-name, tp-layers, tp-meta) is removed.
tp-managed-layer-diagnostics / tp-managed-buffer-diagnostics / tp-managed-diagnostics
(tp-managed-layer-diagnostics LAYER-NAME)
(tp-managed-buffer-diagnostics &optional BUFFER)
(tp-managed-diagnostics)
These functions are read-only diagnostics. They report layers, buffers, entries, stored args, registry state, errors, and theme diagnostics. tp-managed-diagnostics includes a :theme plist with generation, last hook source, refresh mode, refreshed ranges, and errors. Theme enable/disable hooks increment the generation and use conservative refresh diagnostics; this is lifecycle evidence, not a benchmark.
tp-layer-transaction
(tp-layer-transaction START END OBJECT FUNCTION &optional NOERROR)
Runs FUNCTION over a managed range. On success it returns a structured plist with :status ok, :ok t, FUNCTION's value in :result, an operation id, the range, and changed ranges. On error it restores the exact pre-transaction text/property snapshot; by default it signals tp-layer-transaction-error, while NOERROR returns the structured failure plist with rollback status.
(progn
(tp-layer-reset)
(define-tp tx-base () '(face bold))
(define-tp tx-temp () '(face italic))
(with-temp-buffer
(insert "abcd")
(let ((result
(tp-layer-transaction
1 4 (current-buffer)
(lambda () (tp-put-layer 1 3 'tx-base 0)))))
(list (plist-get result :status)
(plist-get result :range)
(tp-at 1 'face)))))
;; => (ok (1 . 4) bold)
Theme generation and managed diagnostics are verified as lifecycle behavior. Reproducible benchmark evidence is recorded in docs/BENCHMARKS.md; those timings are advisory baseline data, not release thresholds.
Property Layer Merging
tp-merge-layers - Merge Multiple Layers
;; Buffer/string region
(tp-merge-layers START END NEW-LAYER-NAME '(IDX1 LAYER-NAME1 IDX2 ...) OBJECT)
;; Entire string
(tp-merge-layers STRING NEW-LAYER-NAME '(IDX1 LAYER-NAME1 IDX2 ...))
Merge specified layers into a new layer. Earlier layers in the list take precedence.
Hidden layers (0.3.0): hidden matched layers are merged away with the
rest but contribute no properties to the merged layer, so a merge can
never render what was hidden. When every matched layer is hidden, the
merged layer keeps their merged properties but carries the tp-hidden flag
itself — the data is preserved without un-hiding anything, and
tp-show-layer on the merged layer renders it. Returns the number of
property runs modified (0 = no listed layer matched).
Examples:
;; Merge layer1 and layer2 into merged-layer
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(help-echo "tip"))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-merge-layers 1 10 'merged-layer '(layer1 layer2))
(tp-at 1 'tp-name)))
;; => merged-layer
;; Merge by index
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(help-echo "tip"))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-merge-layers 1 10 'merged '(0 1))
(tp-layer-count 1 10)))
;; => 1
;; A hidden layer's properties never leak into the merge
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(help-echo "tip"))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-hide-layer 1 10 'layer2)
(tp-merge-layers 1 10 'merged '(layer1 layer2))
(list :face (tp-at 1 'face)
:help (tp-at 1 'help-echo)
:name (tp-at 1 'tp-name))))
;; => (:face bold :help nil :name merged) ; layer2 was hidden
tp-flatten-layers - Flatten All Layers
;; Buffer/string region
(tp-flatten-layers START END NAME OBJECT)
;; Entire string
(tp-flatten-layers STRING NAME)
Flatten all layers into a single layer with the given name.
Hidden layers (0.3.0): hidden layers are discarded, mirroring
image-editor flatten semantics — only the visible layers' properties merge
into the result, so flattening can never render what was hidden. When
every layer of a run is hidden, the run's properties are cleared entirely
(bare text), consistent with the all-hidden rendering of tp-hide-layer.
Returns the number of property runs modified (0 = no run had layers).
Examples:
;; Flatten all layers into 'flat-layer
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(help-echo "tip"))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-flatten-layers 1 10 'flat-layer)
(tp-at 1 'tp-name)))
;; => flat-layer
;; Flatten with nil name (unnamed layer)
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-flatten-layers 1 10 nil)
(tp-at 1 'tp-name)))
;; => nil
;; Hidden layers are discarded by flatten
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-hide-layer 1 10 'highlight)
(tp-flatten-layers 1 10 'flat)
(list (tp-at 1 'face) (tp-at 1 'tp-name))))
;; => (default flat) ; highlight's background is gone
Property Layer Query Functions
tp-layer-list - List All Layers
(tp-layer-list START END &optional OBJECT)
Get list of all layer names in region.
Examples:
(progn
(tp-layer-reset)
(define-tp highlight () '(face (:background "yellow")))
(define-tp base () '(face default))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-layer-list 1 10)))
;; => (highlight base)
tp-layer-count
(tp-layer-count START END &optional OBJECT)
Count layers in region.
Examples:
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-layer-count 1 10)))
;; => 2
tp-layer-exists-p
(tp-layer-exists-p START END NAME &optional OBJECT)
Check if layer exists in region.
Examples:
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(list (tp-layer-exists-p 1 10 'layer1)
(tp-layer-exists-p 1 10 'layer2))))
;; => (t nil)
tp-layer-top
(tp-layer-top START END &optional OBJECT)
Get name of the top layer. The topmost layer is reported in stack
order, even when it is hidden (see
tp-hide-layer);
use tp-layer-stack-at to distinguish hidden layers from visible ones.
Examples:
(progn
(tp-layer-reset)
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
(tp-layer-top 1 10)))
;; => layer2
tp-layer-stack-at - Full Stack at a Position
(tp-layer-stack-at POS &optional OBJECT)
Return the full ordered layer stack at one position (new in 0.3.0), as a
list with one element per layer, topmost first, where each element is a
cons (NAME . PROPS):
- NAME is the layer's
tp-namesymbol, or nil for an unnamed layer. - PROPS is the layer's property plist without its
tp-nameentry. A hidden layer is distinguishable by atp-hiddenentry with value t in PROPS; visible layers never carry one.
Hidden layers are included at their stack position. Returns nil for bare text. POS is in OBJECT's native coordinates (0-based for strings, 1-based for buffers); OBJECT is a string, a buffer, or nil for the current buffer.
Examples:
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-layer-stack-at 1)))
;; => ((highlight . (face (:background "yellow")))
;; (base . (face default)))
;; Hidden layers carry a `tp-hidden' entry in PROPS
(progn
(tp-layer-reset)
(define-tp base () '(face default))
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'base)
(tp-push-layer 1 10 'highlight)
(tp-hide-layer 1 10 'highlight)
(tp-layer-stack-at 1)))
;; => ((highlight . (tp-hidden t face (:background "yellow")))
;; (base . (face default)))
;; Bare text has no stack
(with-temp-buffer
(insert "Hello")
(tp-layer-stack-at 1))
;; => nil
tp-add-to-layers - Add Properties to Specific Layers
;; Buffer/string region
(tp-add-to-layers IDX-OR-LAYER-NAME-LIST START END PLIST &optional OBJECT)
;; Entire string
(tp-add-to-layers IDX-OR-LAYER-NAME-LIST STRING PROP VAL ...)
Add or merge properties to specific layers in a region or string.
- IDX-OR-LAYER-NAME-LIST is a list of layer indices (integers) or layer names (symbols). For indices: 0 means top layer, -1 means bottom layer.
- Properties are deeply merged into the specified layers (nested plists are merged, not replaced).
- OBJECT defaults to current buffer for region form.
- Like the other stack mutators (and unlike
tp-set), the string form modifies STRING in place and returns that same mutated string. For buffers, returns nil.
Examples:
(progn
(tp-layer-reset)
(define-tp layer1 () '(face (:foreground "red")))
(define-tp layer2 () '(face (:foreground "blue")))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'layer1)
(tp-push-layer 1 10 'layer2)
;; Add underline to both layers
(tp-add-to-layers '(0 1) 1 10 '(face (:underline t)))
(tp-at 5)))
;; Both layers now have underline merged with their colors
tp-add-to-all-layers - Add Properties to All Layers
;; Buffer/string region
(tp-add-to-all-layers START END PLIST &optional OBJECT)
;; Entire string
(tp-add-to-all-layers STRING PROP VAL ...)
Add or merge properties to all layers in a region or string.
- Properties are deeply merged into all existing layers.
- OBJECT defaults to current buffer for region form.
- Like the other stack mutators (and unlike
tp-set), the string form modifies STRING in place and returns that same mutated string. For buffers, returns nil.
Examples:
(let ((str (copy-sequence "Hello World")))
(define-tp layer1 () '(face bold))
(define-tp layer2 () '(face italic))
(tp-push-layer 0 5 'layer1 str)
(tp-push-layer 0 5 'layer2 str)
;; Add underline to all layers
(tp-add-to-all-layers 0 5 '(face (:underline t)) str)
str)
tp-intervals - Get Text Property Intervals
(tp-intervals START END &optional OBJECT ABSOLUTE)
Get all text property intervals from START to END in OBJECT.
- Returns a list of (START END PROPERTIES) for each interval, including gap intervals with no properties, whose PROPERTIES is nil.
- For buffer input, START and END are 1-based buffer positions but the
returned positions are by default 0-based offsets relative to START
(the legacy convention). With ABSOLUTE non-nil (new in 0.3.0) they are
native 1-based buffer positions instead, directly reusable in other tp
calls (
tp-set,tp-remove, ...) without offset arithmetic. For strings, positions are always absolute 0-based indices; ABSOLUTE changes nothing. - Uses
object-intervals(requires Emacs 28.1+). - OBJECT can be a buffer or string; nil defaults to current buffer.
Examples:
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
(tp-set 7 12 '(face italic))
(tp-intervals 1 12))
;; => ((0 5 (face bold)) (5 6 nil) (6 11 (face italic)))
;; positions are offsets from START; (5 6 nil) is the unpropertized gap
;; ABSOLUTE - native buffer coordinates
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
(tp-set 7 12 '(face italic))
(tp-intervals 1 12 nil t))
;; => ((1 6 (face bold)) (6 7 nil) (7 12 (face italic)))
;; ABSOLUTE positions feed straight back into other tp calls
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
(dolist (iv (tp-intervals 1 12 nil t))
(when (eq (plist-get (nth 2 iv) 'face) 'bold)
(tp-add (nth 0 iv) (nth 1 iv) '(help-echo "bold text"))))
(tp-at 1 'help-echo))
;; => "bold text"
tp-intervals-map - Apply Function to Intervals
(tp-intervals-map FUNCTION START END &optional OBJECT ABSOLUTE)
Apply FUNCTION to all intervals between START and END in OBJECT.
- FUNCTION receives four arguments: interval-start, interval-end,
top-props (the directly rendered properties, with the
tp-layersentry removed), and below-props-lst (thetp-layersvalue: the stored layer plists buried below the rendered top layer — while any layer is hidden it holds the whole ordered stack; seetp-layer-stack-atfor the decoded view). - Intervals with no properties are visited too, with nil top-props
(positions follow the same coordinate convention as
tp-intervals, including the ABSOLUTE argument, new in 0.3.0). - OBJECT can be a buffer or string; nil defaults to current buffer.
- Returns list of function results (nil results are removed).
Examples:
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
(tp-set 7 12 '(face italic))
(tp-intervals-map
(lambda (start end props belows)
(list start end (plist-get props 'face)))
1 12))
;; => ((0 5 bold) (5 6 nil) (6 11 italic))
;; ABSOLUTE - FUNCTION receives native buffer positions
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
(tp-set 7 12 '(face italic))
(tp-intervals-map
(lambda (start end props belows)
(list start end (plist-get props 'face)))
1 12 nil t))
;; => ((1 6 bold) (6 7 nil) (7 12 italic))
tp-region-layer-props - Get Layer Properties in Region
(tp-region-layer-props START END LAYER-NAME &optional OBJECT)
Return layer properties for LAYER-NAME in region from START to END.
- Returns a list of (START END PROPERTIES) for matching intervals.
- OBJECT defaults to current buffer.
Examples:
(progn
(tp-layer-reset)
(define-tp highlight () '(face (:background "yellow")))
(with-temp-buffer
(insert "Hello World Test")
(tp-push-layer 1 6 'highlight)
(tp-push-layer 12 16 'highlight)
(tp-region-layer-props 1 16 'highlight)))
;; => ((1 6 (face (:background "yellow") tp-name highlight))
;; (12 16 (face (:background "yellow") tp-name highlight)))
tp-plist - Get All Properties in Region
;; Buffer/string region
(tp-plist START END &optional OBJECT)
;; Entire string
(tp-plist STRING)
Get a property list of all properties present in a region or string.
- Returns a single merged plist of the properties found in the range; when the same property occurs in several intervals, the value from the later interval wins.
- OBJECT defaults to current buffer for region form.
Examples:
(with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold help-echo "Tip"))
(tp-set 7 12 '(face italic))
(tp-plist 1 12))
;; => (help-echo "Tip" face italic) ; later interval's face wins
tp-empty-p - Check if Object Has Properties
(tp-empty-p &optional OBJECT)
Return t if OBJECT has no text properties.
- OBJECT can be a string or buffer; nil defaults to current buffer.
- Uses
object-intervals(requires Emacs 28.1+).
Examples:
(tp-empty-p "plain text") ; => t
;; Whole-string tp-set is non-destructive: the original stays empty
(let* ((str "text")
(new (tp-set str 'face 'bold)))
(list (tp-empty-p str) (tp-empty-p new)))
;; => (t nil)
tp-with-current-buffer / tp-pop-to-buffer / tp-switch-to-buffer
(tp-with-current-buffer BUFFER-OR-NAME BODY...)
(tp-pop-to-buffer BUFFER-OR-NAME BODY...)
(tp-switch-to-buffer BUFFER-OR-NAME BODY...)
Convenience macros for operating on and displaying propertized content:
tp-with-current-bufferevaluates BODY in BUFFER-OR-NAME withinhibit-read-onlybound to t. Useful for modifying read-only display buffers.tp-pop-to-buffercreates (or reuses) BUFFER-OR-NAME, erases it, evaluates BODY inside it, then makes it read-only and displays it withpop-to-buffer. Pressqin the displayed buffer to quit its window.tp-switch-to-bufferis the same but displays the buffer withswitch-to-buffer.
Example:
(tp-pop-to-buffer "*tp-demo*"
(insert (tp-set "Important" 'face '(:foreground "red" :weight bold))
" message\n"))
;; Displays *tp-demo* with the propertized text; `q' quits the window
Color Palette System
tp-palette.el ships a set of named color palettes with separate light-mode
and dark-mode colors, and tp-builtins.el exposes them through the built-in
parameterized tp-palette layer (as in (tp-set "emacs" 'tp-palette 'info)).
-
tp-palette-alist(variable) — alist of(NAME . PLIST)palette definitions; the single source of truth for palette lookups. Each PLIST maps:fg,:bg, and:borderto colors. -
define-tp-palette— register (or update) a palette (since 0.3.0 also available as the prefix-conforming aliastp-define-palette):(define-tp-palette my-brand :fg ("#0969da" . "#58a6ff") ; ("light" . "dark") :bg ("#ddf4ff" . "#1f3d5c")) -
tp-palette-color(new in 0.3.0) — the palette accessor: get a palette's:fg/:bg/:bordercolor, resolved for the current light/dark theme. Returns nil for a missing palette or key:(tp-palette-color 'info :fg) ;; => "#0969da" on a light theme, "#58a6ff" on a dark theme (tp-palette-color 'no-such-palette :fg) ;; => nil -
tp-palette-has-p(new in 0.3.0) — the palette predicate: with just SYMBOL, test whether it names a registered palette; with KIND one of:fg/:bg/:border, additionally require that key in its definition (a defined key may still resolve to no color for the current theme — usetp-palette-colorwhen the resolved color matters):(list (tp-palette-has-p 'info) (tp-palette-has-p 'info :border) (tp-palette-has-p 'no-such-palette)) ;; => (t t nil)The older per-key conveniences remain as compatible wrappers:
tp-palette-fg-color/tp-palette-bg-color/tp-palette-border-color(fixed-KEY variants oftp-palette-color),tp-palette-p(nil-KINDtp-palette-has-p), and the suffixed-name predicatestp-palette-fg-p/tp-palette-bg-p/tp-palette-fbg-p/tp-palette-border-p, which answer a different question: whether a variant name likeinfo-fgdenotes a registered palette (thetp-palettelayer's convention). -
tp-palette-show— interactive command that displays a gallery buffer of every registered palette and its-fg/-bg/-fbg/-bordervariants (qquits). -
tp-parse-color— resolve a color spec for the current theme. Accepts a plain color string, a("light" . "dark")cons (either side may be nil), or a(:light L :dark D)plist:(tp-parse-color "red") ; => "red" (tp-parse-color '("white" . "black")) ; => "white" on a light theme, ; "black" on a dark theme
Note: tp-layer-reset clears every layer definition, including built-in
layers like tp-palette.
Practical Examples
Syntax Highlighting with Multiple Layers
;; Complete example that can be run in a buffer
(progn
(tp-layer-reset)
;; Define layers for different highlighting purposes
(define-tp code-base ()
'(face font-lock-keyword-face))
(define-tp code-error ()
'(face (:underline (:color "red" :style wave))
help-echo "Syntax error"))
(define-tp code-debug ()
'(face (:background "dark blue")))
(with-temp-buffer
(insert (make-string 100 ?x)) ; Create 100-char buffer
;; Apply base highlighting
(tp-push-layer 1 100 'code-base)
;; Add error highlight on problematic code
(tp-push-layer 50 60 'code-error)
;; Check the top layer at position 55
(tp-layer-top 50 60)))
;; => code-error
;; Toggle function (for use in real buffers)
(defun toggle-error-view (start end)
"Toggle between error and normal view."
(interactive "r")
(tp-rotate-layer start end))
Status Indicator
;; Complete example with layer group
(progn
(tp-layer-reset)
;; Define status layers as a group
(define-tp status-todo () '(face (:foreground "gray")))
(define-tp status-progress () '(face (:foreground "yellow")))
(define-tp status-done () '(face (:foreground "green")))
(define-tps task-status () 'status-todo 'status-progress 'status-done)
;; Check group is defined
(length (tp-group-props 'task-status)))
;; => 3
;; Cycle through statuses (for use in real buffers)
(defun cycle-task-status ()
"Cycle through task status layers on current line."
(interactive)
(tp-rotate-layer (line-beginning-position) (line-end-position)))
Temporary Highlights
;; Define temporary highlight layer
(progn
(tp-layer-reset)
(define-tp temp-highlight ()
'(face (:background "yellow")))
(tp-layer-props 'temp-highlight))
;; => (face (:background "yellow"))
;; Flash function (for use in real buffers)
(defun flash-region (start end)
"Flash a region temporarily."
(tp-push-layer start end 'temp-highlight)
(run-with-timer 0.5 nil
(lambda (s e)
(tp-delete-layer s e 'temp-highlight))
start end))
Reactive Text Properties
📖 For a comprehensive guide with detailed examples, see Reactive Text Properties Complete Guide
📖 For advanced optimization features, see Reactive System Optimization
Reactive Text Properties is tp.el's groundbreaking innovation that brings reactive programming paradigms to Emacs text properties. Inspired by modern frontend frameworks like Vue.js, this feature enables text properties to automatically update when underlying variable values change.
Core Concept
Traditional text property manipulation requires manually updating all affected text regions whenever you want to change a property value. With reactive text properties, you simply define a variable relationship once, and tp.el handles all updates automatically:
;; Traditional approach (manual updates required)
(defvar my-color "red")
(tp-set 1 10 '(face (:foreground "red")))
;; To change color, you must manually update every region:
(setq my-color "blue")
(tp-set 1 10 '(face (:foreground "blue"))) ; Manual!
;; Reactive approach (automatic updates)
(defvar my-color "red")
(define-tp my-layer ()
:props '(face (:foreground $my-color)))
(tp-push-layer 1 10 'my-layer)
;; Just change the variable - all text updates automatically!
(setq my-color "blue") ; All regions with my-layer update instantly!
How It Works
-
Reactive Variables: Any symbol prefixed with
$in:propsis treated as a reactive variable. The$is stripped to get the actual variable name. -
Variable Watchers: tp.el uses Emacs's
add-variable-watcherto monitor changes to reactive variables. -
Automatic Updates: When a reactive variable changes via
setq, all text regions using layers that depend on that variable are automatically updated with the new property values.
Defining Reactive Layers
Basic Reactive Layer
(defvar highlight-color "yellow")
(define-tp my-highlight ()
:props '(face (:background $highlight-color)))
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'my-highlight)
;; Text is highlighted in yellow
(setq highlight-color "cyan")
;; Text is now highlighted in cyan - automatically!
)
Multiple Reactive Variables
(defvar fg-color "white")
(defvar bg-color "black")
(define-tp themed-text ()
:props '(face (:foreground $fg-color :background $bg-color)))
;; Changing either variable updates the text
(setq fg-color "yellow") ; Updates foreground
(setq bg-color "navy") ; Updates background
:data - Additional Reactive State
The :data keyword defines additional reactive variables that aren't directly used in :props but can trigger computed value updates or be watched:
(define-tp user-info ()
:props '(help-echo $full-name)
:data '(first-name last-name) ; Not used directly in props
:compute '((full-name (lambda () (concat first-name " " last-name)))))
With Initial Values:
You can specify initial values using cons cells:
(define-tp user-info ()
:props '(help-echo $full-name)
:data '((first-name . "John") (last-name . "Doe"))
:compute '((full-name (lambda () (concat first-name " " last-name)))))
;; first-name is now "John", last-name is now "Doe"
:compute - Computed Properties
The :compute keyword creates derived values that are automatically recalculated when their dependencies change:
(define-tp progress-display ()
:props '(display $progress-text face (:foreground $progress-color))
:data '((current . 0) (total . 100))
:compute '((progress-text (lambda () (format "%d%%" (/ (* current 100) total))))
(progress-color (lambda ()
(cond ((< current 30) "red")
((< current 70) "yellow")
(t "green"))))))
;; Update progress
(setq current 50)
;; progress-text becomes "50%" and progress-color becomes "yellow" automatically!
:watch - Side Effect Callbacks
The :watch keyword lets you execute callbacks when reactive variables change:
(define-tp monitored-layer ()
:props '(face (:foreground $status-color))
:watch '((status-color
(lambda (new-val old-val layer-name)
(message "Layer %s: color changed from %s to %s"
layer-name old-val new-val)))))
(setq status-color "red")
;; Message: "Layer monitored-layer: color changed from nil to red"
(setq status-color "green")
;; Message: "Layer monitored-layer: color changed from red to green"
:transform - Value Transformation
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:
;; 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.00
;; 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-textstring value - Returns the transformed string for display
- Is applied both on initial display and reactive updates
- Must return a string; failures and non-string results propagate instead of rendering stale input
Compute functions follow the same business-error rule. Watch callbacks are
observers: their failures are isolated so rendering continues, and structured
records are appended newest-first to tp-reactive-observer-errors.
Anonymous Reactive Layers
You can use reactive variables even without define-tp. When you use $-prefixed symbols in an anonymous plist, tp.el automatically generates a unique layer name:
(defvar my-face-color "blue")
;; Anonymous reactive layer - tp-name is auto-generated
(tp-set 1 10 '(face (:foreground $my-face-color)))
;; The layer is now reactive - changing the variable updates the text
(setq my-face-color "red")
Layer Name Resolution in APIs
All text property APIs (tp-set, tp-match-set, tp-regexp-set, etc.) now accept layer names directly:
(define-tp warning-style ()
:props '(face (:foreground "orange" :weight bold)))
;; Use layer name instead of plist
(tp-set 1 10 'warning-style)
;; Works with all matching functions
(tp-match-set "TODO" 'warning-style)
(tp-regexp-set "[0-9]+" 'warning-style)
This direct use is template expansion for non-reactive layers: the
definition becomes ordinary text properties and does not retain tp-name.
Use tp-push-layer or tp-put-layer for a managed mount that can later
be queried, moved, hidden, deleted by name, or refreshed after a layer
redefinition. Parameterized mounted entries retain their arguments and use
them to refresh existing instances after redefinition.
Reactive Layer Groups
Layer groups can also use reactive features:
(define-tps status-indicators ()
'("success" :props (face (:foreground $success-color))
:data ((success-color . "green")))
'("warning" :props (face (:foreground $warning-color))
:data ((warning-color . "orange")))
'("error" :props (face (:foreground $error-color))
:data ((error-color . "red"))))
Batched Updates
When modifying multiple reactive variables simultaneously, each setq triggers a separate buffer update. Use tp-with-batch-updates to consolidate all changes and apply them once at the end:
(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
Benefits of batched updates:
- Reduces redundant buffer modifications
- Improves performance when changing multiple variables
- Ensures consistent state when multiple variables are interdependent
Layer-Buffer Registry & Lifecycle
Since 0.3.0 the reactive engine keeps a layer→buffer registry: every
buffer-mutating write path that stamps a layer (the tp-set family, the
stack mutators, the match/regexp appliers) registers the target buffer as
showing that layer, and a reactive update visits only the registered
buffers instead of scanning the whole (buffer-list). Killed buffers are
pruned automatically. When a layer has no registry entry at all, one
learning full scan falls back to the old behavior and registers every
buffer where the layer is actually found.
Updates reach a layer's regions even while the layer is hidden or
buried below other layers in a stack: the stored tp-layers entry is
updated in place, so tp-show-layer (or raising the layer) always reveals
current values.
tp-reactive-layer-buffers - Inspect the Registry
(tp-reactive-layer-buffers LAYER-NAME)
Return the live buffers registered as showing LAYER-NAME — a list (possibly
empty, meaning "known: no buffer shows this layer") — or the symbol
unknown when the layer has no registry entry at all:
(progn
(tp-layer-reset)
(defvar reg-color "red")
(define-tp reg-layer ()
:props '(face (:foreground $reg-color)))
(tp-reactive-layer-buffers 'reg-layer))
;; => unknown ; never applied to any buffer yet
(with-temp-buffer
(rename-buffer "demo-buffer" t)
(insert "Hello")
(tp-push-layer 1 6 'reg-layer)
(mapcar #'buffer-name (tp-reactive-layer-buffers 'reg-layer)))
;; => ("demo-buffer")
tp-reactive-track-buffer - Close the String-Insert Gap
(tp-reactive-track-buffer &optional BUFFER) ; interactive
Known gap: inserting an already-propertized string into a buffer
bypasses the buffer operations that register buffers, so that buffer is
missing from the registry until a learning full scan finds it. Call
tp-reactive-track-buffer after such an insert: it scans BUFFER (default:
the current buffer) for layer regions — rendered top layers as well as
layers buried or hidden inside tp-layers stack storage — registers the
buffer for each, and returns the layer names found in buffer order:
(let ((s (tp-set "hello" 'reg-layer))) ; propertized string, detached
(with-temp-buffer
(insert s) ; bypasses registration
(tp-reactive-track-buffer)))
;; => (reg-layer) ; buffer now registered for reg-layer
tp-gc-anonymous-layers - Collect Unused Anonymous Layers
(tp-gc-anonymous-layers) ; interactive
Anonymous reactive layers are interned: an
equal props spec reuses its registry entry instead of minting a new layer
on every tp-set. tp-gc-anonymous-layers undefines every interned
anonymous layer that no registered live buffer still displays (buried and
hidden layers count as alive) and returns the collected layer names:
(defvar tmp-color "green")
(let ((buf (generate-new-buffer "*gc-demo*")))
(with-current-buffer buf
(insert "Hello")
(tp-set 1 6 '(face (:foreground $tmp-color)))) ; anonymous layer
(kill-buffer buf)
(tp-gc-anonymous-layers))
;; => (tp-anon-1) ; the collected names (the counter varies)
Conservative unknown semantics: a layer whose registry state is
unknown — never seen in any buffer through the registering paths, for
example referenced only by detached strings — is deliberately kept. A
layer becomes collectable only after it was registered for at least one
buffer and none of the registered buffers still shows it (e.g. all killed).
Call tp-reactive-track-buffer after inserting propertized strings so
their buffers are registered too.
Minimal-Diff tp-text Re-Rendering
Reactive tp-text replacements edit only the differing span of the old
and new text (inserting before deleting), so point and markers in unchanged
text keep their positions; point inside the edited span lands at the edit
start. An update to an identical value is a true no-op: no text edit,
no property churn, and the buffer-modified flag is untouched.
(progn
(tp-layer-reset)
(defvar counter-val "0")
(define-tp counter-label ()
:props '(tp-text $counter-val))
(with-temp-buffer
(insert "count: 0 items")
(tp-set 8 9 'counter-label)
(let ((m (copy-marker 10))) ; marker on the "i" of "items"
(setq counter-val "9") ; only the digit is edited
(list (buffer-substring-no-properties 1 (point-max))
(char-after m)))))
;; => ("count: 9 items" ?i) ; the marker still points at its character
;; Identical-value updates do not touch the buffer at all
(with-temp-buffer
(insert "count: 9 items")
(tp-set 8 9 'counter-label)
(set-buffer-modified-p nil)
(setq counter-val "9") ; same text as displayed
(buffer-modified-p))
;; => nil
Debug Mode
tp.el provides a debug mode to help understand reactive update flow:
;; Enable debug mode
(setq tp-debug-mode t)
;; Also show debug info in minibuffer (optional)
(setq tp-debug-echo t)
;; View debug log
(tp-debug-show)
;; Clear debug log
(tp-debug-clear)
Debug log includes:
- Variable change notifications (old → new value)
- Layer update tracking
- Batch update start/end
- Transform application info
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)
Resetting Reactive State
To clear all reactive dependencies and watchers:
(tp-reactive-reset) ; Clears only reactive state
(tp-layer-reset) ; Clears layers, groups, AND reactive state
Complete Example: Theme-Aware Text
;; Define theme variables
(defvar theme-fg "white")
(defvar theme-bg "black")
(defvar theme-accent "cyan")
;; Define theme-aware layers - each one references a theme variable
(define-tp code-text ()
:props '(face (:foreground $theme-fg :background $theme-bg)))
(define-tp code-keyword ()
:props '(face (:foreground $theme-accent :weight bold)))
;; Apply layers to code in the current buffer
(tp-set (point-min) (point-max) 'code-text)
(tp-match-set '("defun" "defvar" "let" "if" "when") 'code-keyword)
;; Switch to light theme - just change the variables!
(defun switch-to-light-theme ()
(interactive)
(setq theme-fg "black")
(setq theme-bg "white")
(setq theme-accent "blue"))
;; Switch to dark theme
(defun switch-to-dark-theme ()
(interactive)
(setq theme-fg "white")
(setq theme-bg "black")
(setq theme-accent "cyan"))
;; After `switch-to-light-theme', keywords turn blue and the rest of the
;; code turns black-on-white - every region re-renders automatically
License
GNU General Public License v3 or later. See the LICENSE file.
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
tp.el - Making text properties powerful and easy to use