A powerful text properties manipulation library with an innovative property layer system
Features • Installation • Quick Start • API Reference • Property Layer System • Reactive Text Properties • 中文文档
- Overview
- Features
- Requirements
- Installation
- API Reference
- The Property Layer System
- Reactive Text Properties
- Practical Examples
- License
- Contributing
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:
- 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
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
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 intelligentlyThis 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 str 'face :underline :style) ; => wave (tp-at 5 '(face :box :color)) ; => "blue" ;; Get multiple nested keys (tp-get str 'face :underline '(:color :style)) ;; => ((: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
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
tp-define-layer - ✅ 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(up/down),tp-rotate-layer(rotate),tp-pin-layer(pin to top),tp-switch-layer(swap) - 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
;; Property layer usage example
(tp-define-layer 'highlight '(face (:background "yellow")))
(tp-define-layer '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 visibleNative 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)))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)
;; Define a layer with reactive properties
(defvar my-color "red") ;; Reactive variable
(tp-define-layer '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
(tp-define-layer 'full-name-layer
:props '(help-echo $full-name face (:foreground $name-color))
:data ((first-name . "John") (last-name . "Doe")) ;; With initial values
: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)))))- ✅ Range Search:
tp-searchreturns a list of all matching intervals - ✅ N-times Search:
tp-forward/tp-backwardsupport searching forward/backward N times - ✅ Search and Execute:
tp-forward-do/tp-backward-dosearch and execute function on matched text - ✅ 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 my-string 'marker)- Emacs 28.1+ (uses
object-intervalsfunction) - dash.el (list manipulation utilities)
;; 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")A complete overview of all tp.el functions organized by category:
| 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-remove |
Remove a property or sub-property |
tp-clear |
Clear all text properties from a region |
| Function | Description |
|---|---|
tp-match-set |
Set properties on string pattern matches |
tp-match-reset |
Reset all properties on string matches |
tp-match-add |
Add/merge properties on string matches |
tp-regexp-set |
Set properties on regexp matches |
tp-regexp-reset |
Reset all properties on regexp matches |
tp-regexp-add |
Add/merge properties on regexp matches |
| Function | Description |
|---|---|
tp-search-forward |
Raw wrapper for text-property-search-forward |
tp-search-backward |
Raw wrapper for text-property-search-backward |
tp-forward |
Search forward N times for text with property (buffers and strings) |
tp-backward |
Search backward N times for text with property (buffers and strings) |
tp-forward-do |
Apply function to last match in forward search (with optional start/end range) |
tp-backward-do |
Apply function to last match in backward search (with optional start/end range) |
tp-search |
Search all matching properties in range or string |
tp-search-map |
Apply function to all matches (with optional start/end range) |
| Function | Description |
|---|---|
tp-define-layer |
Define a single layer with optional reactive features (:props, :data, :watch, :compute) |
tp-define-layer-group |
Define a group of layers with optional reactive features |
define-tp |
Convenience macro for defining layers (supports parameterized layers) |
define-tp-group |
Convenience macro for defining layer groups |
tp-layer-props |
Get properties for a layer |
tp-group-props |
Get properties for all layers in a group |
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 |
| Function | Description |
|---|---|
tp-put-layer |
Set layer at specific index position |
tp-push-layer |
Push layer to top of stack |
| Function | Description |
|---|---|
tp-delete-layer |
Delete layer by name or index |
tp-pop-layer |
Remove top layer |
| Function | Description |
|---|---|
tp-move-layer |
Move a layer from one position to another |
tp-raise-layer |
Move layer up/down by N positions |
tp-rotate-layer |
Cycle layers (top goes to bottom) |
tp-pin-layer |
Pin a layer to top (make visible) |
tp-switch-layer |
Swap positions of two layers |
| Function | Description |
|---|---|
tp-merge-layers |
Merge specified layers into a new layer |
tp-flatten-layers |
Flatten all layers into a single layer |
| 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 (visible) layer |
tp-region-layer-props |
Get properties for a specific layer in region |
| 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 |
| Function | Description |
|---|---|
tp-intervals |
Get all text property intervals in a region |
tp-intervals-map |
Apply function to all intervals in a region |
tp-plist |
Get all properties present in a region |
tp-empty-p |
Check if object has no text properties |
Set text properties on a string or buffer region. Replaces only the specified properties, preserving others.
;; Current buffer (properties as a list)
(tp-set START END '(PROPERTY VALUE ...))
(tp-set START END LAYER-NAME)
;; Specific buffer or string
(tp-set START END '(PROPERTY VALUE ...) OBJECT)
(tp-set START END LAYER-NAME OBJECT)
;; Entire string (flat properties or layer name)
(tp-set STRING PROPERTY VALUE ...)
(tp-set STRING LAYER-NAME)LAYER-NAME can be a symbol representing a layer defined by tp-define-layer or a group defined by tp-define-layer-group.
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
(tp-define-layer '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"))
(tp-set 1 10 '(face italic) my-buffer)
(kill-buffer my-buffer))
;; => (1 . 10)
;; Set properties on a string (0-indexed)
(let ((my-string (tp-set 0 5 '(face italic) "Hello World")))
my-string)
;; => #("Hello World" 0 5 (face italic))
;; Set properties on entire string
(tp-set "Hello" 'face 'bold 'mouse-face 'highlight)
;; => #("Hello" 0 5 (face bold mouse-face highlight))
;; Use a defined layer name on entire string
(tp-define-layer 'my-style
:props '(face (:foreground $my-color))
:data ((my-color . "blue")))
(tp-set " " 'my-style)
;; => #(" " 0 1 (tp-name my-style face (:foreground "blue") ...))Completely replace ALL text properties with the specified ones.
(tp-reset START END '(PROPERTY VALUE ...) &optional OBJECT)
(tp-reset START END LAYER-NAME &optional OBJECT)
(tp-reset STRING PROPERTY VALUE ...)LAYER-NAME can be a symbol representing a layer defined by tp-define-layer or a group defined by tp-define-layer-group.
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 string
(tp-reset "Hello" 'face 'italic)
;; => #("Hello" 0 5 (face italic))
;; Use a defined layer name
(tp-define-layer '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-styleAdd or update properties with deep merge support for nested plists.
(tp-add START END '(PROPERTY VALUE ...) &optional OBJECT)
(tp-add START END LAYER-NAME &optional OBJECT)
(tp-add STRING PROPERTY VALUE ...)LAYER-NAME can be a symbol representing a layer defined by tp-define-layer or a group defined by tp-define-layer-group.
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")
;; Face prepending - symbol faces are prepended to face list
(let ((str (tp-set "Hello" 'face 'bold)))
(tp-add str 'face 'shadow)
(tp-at 0 'face str))
;; => (shadow bold)
;; Use a defined layer name
(tp-define-layer '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-styleGet 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)));; 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"Remove a property or nested sub-property from a region or entire string.
;; Remove entire property (buffer)
(tp-remove START END PROPERTY &optional OBJECT)
;; Remove sub-property (buffer)
(tp-remove START END '(PROPERTY SUB-KEY) &optional OBJECT)
;; Remove nested sub-properties (buffer)
(tp-remove START END '(PROPERTY SUB-KEY (NESTED-KEYS...)) &optional OBJECT)
;; Remove from entire string
(tp-remove STRING PROP1 PROP2 ...)
(tp-remove STRING PROPERTY SUB-KEY)
(tp-remove STRING PROPERTY SUB-KEY '(NESTED-KEYS...))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 - multiple properties
(let ((str (tp-set "Hello World" 'face 'bold 'help-echo "tip")))
(tp-remove str 'face 'help-echo)
(tp-at 0 str))
;; => nil
;; Remove sub-property from string
(let ((str (copy-sequence "Hello World")))
(tp-set 0 11 '(face (:foreground "red" :underline t)) str)
(tp-remove str 'face :underline)
(tp-at 0 'face str))
;; => (:foreground "red")
;; Remove nested keys from string
(let ((str (copy-sequence "Hello World")))
(tp-set 0 11 '(face (:underline (:style wave :color "blue"))) str)
(tp-remove str 'face :underline '(:style))
(tp-at 0 '(face :underline) str))
;; => (:color "blue")(tp-clear &optional START END OBJECT)Clear all text properties from a region.
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(tp-match-set PATTERN PLIST &optional OBJECT)
(tp-match-set PATTERN LAYER-NAME &optional OBJECT)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 tp-define-layer or a group defined by tp-define-layer-group.
OBJECT is a buffer or string; nil means current buffer.
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 modified string
(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)))
;; => ((1 . 6) (7 . 12) (14 . 19)) ; Matches "Hello", "world", "Hello"
;; 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
(tp-define-layer '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))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 tp-define-layer or a group defined by tp-define-layer-group.
OBJECT is a buffer or string; nil means current buffer.
(tp-match-reset PATTERN PLIST &optional OBJECT)
(tp-match-reset PATTERN LAYER-NAME &optional OBJECT)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
(tp-define-layer 'alert-style
'(face (:background "red" :foreground "white")))
(with-temp-buffer
(insert "TODO: fix this")
(tp-match-reset "TODO" 'alert-style))
;; => ((1 . 5))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 tp-define-layer or a group defined by tp-define-layer-group.
OBJECT is a buffer or string; nil means current buffer.
(tp-match-add PATTERN PLIST &optional OBJECT)
(tp-match-add PATTERN LAYER-NAME &optional OBJECT)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
(tp-define-layer '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 PATTERN PLIST &optional OBJECT)
(tp-regexp-set PATTERN LAYER-NAME &optional OBJECT)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 tp-define-layer or a group defined by tp-define-layer-group.
OBJECT is a buffer or string; nil means current buffer.
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
(tp-regexp-set "[A-Z]+" '(face bold) "Hello WORLD")
;; => #("Hello WORLD" 6 11 (face bold))
;; Multiple regexps - match both numbers and uppercase letters
(tp-regexp-set '("[0-9]+" "[A-Z]+") '(face bold) "abc 123 XYZ")
;; => #("abc 123 XYZ" 4 7 (face bold) 8 11 (face bold))
;; Use a defined layer name
(tp-define-layer 'number-style
'(face (:foreground "green")))
(with-temp-buffer
(insert "abc 123 def 456")
(tp-regexp-set "[0-9]+" 'number-style))
;; => ((5 . 8) (13 . 16))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 tp-define-layer or a group defined by tp-define-layer-group.
OBJECT is a buffer or string; nil means current buffer.
(tp-regexp-reset PATTERN PLIST &optional OBJECT)
(tp-regexp-reset PATTERN LAYER-NAME &optional OBJECT)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
(let ((str (copy-sequence "abc 123 def")))
(tp-set 4 7 '(help-echo "original") str)
(tp-regexp-reset "[0-9]+" '(face italic) str)
(tp-at 4 str))
;; => (face italic)
;; Use a defined layer name
(tp-define-layer 'code-number
'(face (:foreground "cyan")))
(with-temp-buffer
(insert "abc 123 def 456")
(tp-regexp-reset "[0-9]+" 'code-number))
;; => ((5 . 8) (13 . 16))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 tp-define-layer or a group defined by tp-define-layer-group.
OBJECT is a buffer or string; nil means current buffer.
(tp-regexp-add PATTERN PLIST &optional OBJECT)
(tp-regexp-add PATTERN LAYER-NAME &optional OBJECT)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
(let ((str (copy-sequence "abc 123 def")))
(tp-set 4 7 '(help-echo "number") str)
(tp-regexp-add "[0-9]+" '(face italic) str)
(tp-at 4 str))
;; => (face italic help-echo "number")
;; Use a defined layer name
(tp-define-layer '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))(tp-search-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT)
(tp-search-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT)Raw wrappers for Emacs's text-property-search-forward and text-property-search-backward.
These are low-level search functions that work directly with prop-match objects.
(tp-forward PROPERTY &optional VALUE OBJECT N)
(tp-backward PROPERTY &optional VALUE OBJECT N)Search forward/backward N times for text with PROPERTY.
- N is the number of searches, defaulting to 1.
- VALUE is the optional value to match.
- OBJECT can be a buffer or string; nil defaults to current buffer.
- For buffers, returns the prop-match object from the last successful search.
- For strings, returns a list of (START END VALUE) for all matches found.
Examples:
;; Find next text with 'marker property
(with-temp-buffer
(insert "Hello World Test")
(tp-set 7 12 '(marker t))
(goto-char 1)
(let ((match (tp-forward 'marker)))
(when match
(prop-match-beginning match))))
;; => 7
;; 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 nil my-string 2))
;; => ((0 5 t) (12 17 t))(tp-forward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END)
(tp-backward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END)Search forward/backward for text with PROPERTY and apply FUNCTION only to the last match.
- FUNCTION receives
(TEXT &optional START END)where TEXT is the matched text, START and END are the positions of the match. The return value of FUNCTION replaces the matched text in the string or buffer. - PROPERTY is the text property to search for.
- VALUE is the optional value to match; nil means search for PROPERTY without matching value.
- 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 last (Nth) match found.
- START and END define the search range; defaults are object start and end.
- 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 nil 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 nil my-string 2 6 17)
my-string)
;; => "hello world HELLO" ; Only 1 match in range 6-17
;; 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 nil 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 nil my-string 2)
my-string)
;; => "HELLO world hello" ; The first match (last when searching backward) is upcased;; 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 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 The return value of FUNCTION replaces the matched text in the string or buffer.
- PROPERTY is the text property to search for.
- VALUE is the optional value to match; nil means search for PROPERTY without matching value.
- 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 nil 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 nil 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 nil 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 nil my-string)
my-string)
;; => "HELLO world"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.
┌─────────────────────────────┐
│ TOP LAYER (visible) │ ← idx=0, What you see
├─────────────────────────────┤
│ Middle Layer (hidden) │ ← idx=1, Preserved
├─────────────────────────────┤
│ Bottom Layer (hidden) │ ← idx=-1, Preserved
└─────────────────────────────┘
Define a single text property layer. Supports multiple formats:
Format 1 - Direct plist (no reactive features):
(tp-define-layer 'layer-name
'(face (:background "cyan") line-prefix ">>"))Format 2 - With :props, :data, :watch, and/or :compute (Vue 3 style reactivity):
(tp-define-layer 'layer-name
;; props: $-prefixed symbols are reactive variables; auto-defined if not bound
:props '(face (:foreground $my-color) help-echo $full-name)
;; data: additional reactive variables not used in props; can include initial values
:data '((first-name . "John") (last-name . "Doe"))
;; compute: list of (VAR-NAME FUNCTION) - compute reactive variable values
:compute '((full-name (lambda () (concat first-name " " last-name))))
;; watch: list of (VAR-NAME CALLBACK) - side effects when vars change
:watch '((my-color (lambda (new old layer)
(message "Color changed from %s to %s" old new)))))Reactive Variables:
If any symbol in :props starts with $, it is treated as a reactive variable. Variables in :data are also reactive. All reactive variables are automatically defined as global variables if they are not already bound.
- :data - A list of variable symbols (or cons cells
(SYMBOL . INITIAL-VALUE)) for additional reactive state not in:props. - :compute - A list of
(VAR-SYMBOL COMPUTE-FN)pairs. COMPUTE-FN is evaluated to compute the value of VAR-SYMBOL. Can reference other reactive variables. - :watch - A list of
(VAR-SYMBOL CALLBACK)pairs. CALLBACK is called when VAR-SYMBOL changes, receiving(NEW-VALUE OLD-VALUE LAYER-NAME).
Note: When using :watch, :compute, or :data, you MUST use :props to specify the text properties explicitly.
If a layer with the same name already exists, it will be overwritten with the new definition.
Examples:
;; Define individual layers using Format 1 (direct plist)
(progn
(setq tp-layer-alist nil) ; Reset for clean example
(tp-define-layer 'highlight
'(face (:background "yellow" :foreground "black")))
(tp-layer-props 'highlight))
;; => (face (:background "yellow" :foreground "black") tp-name highlight)
;; Define a reactive layer
(progn
(tp-layer-reset)
(defvar theme-color "blue")
(tp-define-layer 'themed-layer
:props '(face (:foreground $theme-color)))
;; Apply the layer
(with-temp-buffer
(insert "Hello World")
(tp-push-layer 1 10 'themed-layer)
;; Later, change the variable - text updates automatically!
(setq theme-color "red")
(tp-at 1 'face)))
;; => (:foreground "red")
;; Redefine an existing layer (overwrites the old definition)
(progn
(tp-define-layer 'test-layer '(face bold))
(tp-define-layer 'test-layer '(face italic)) ; Overwrites
(tp-layer-props 'test-layer))
;; => (face italic tp-name test-layer)Define a group of multiple layers. Supports multiple formats for each element:
Format 1 - Anonymous layers (named as GROUP-NAME-0, GROUP-NAME-1, etc.):
(tp-define-layer-group 'tp-test-moons
'(display "🌑" face (:height 1.0))
'(display "🌘" face (:height 1.5))
'(display "🌗" face (:height 2.0)))
;; Creates layers: tp-test-moons-0, tp-test-moons-1, tp-test-moons-2Format 2 - Named layers with cons-cell (named as GROUP-NAME-suffix):
(tp-define-layer-group 'tp-test-moons
'("新月" . (display "🌑" face (:height 1.0)))
'("残月" . (display "🌘" face (:height 1.5)))
'("下弦月" . (display "🌗" face (:height 2.0))))
;; Creates layers: tp-test-moons-新月, tp-test-moons-残月, tp-test-moons-下弦月Format 3 - Named layers with :props keyword (named as GROUP-NAME-suffix):
(tp-define-layer-group 'tp-test-moons
'("新月" :props (display "🌑" face (:height 1.0)))
'("残月" :props (display "🌘" face (:height 1.5)))
'("下弦月" :props (display "🌗" face (:height 2.0))))
;; Creates layers: tp-test-moons-新月, tp-test-moons-残月, tp-test-moons-下弦月Format 4 - Named layers with :props, :data, :watch, and/or :compute (Vue 3 style reactivity):
(tp-define-layer-group 'reactive-group
'("reactive" :props (face (:foreground $my-color) help-echo $full-name)
:data ((first-name . "John") (last-name . "Doe"))
:compute ((full-name (lambda () (concat first-name " " last-name))))
:watch ((my-color (lambda (new old layer) (message "Changed!"))))))
;; Creates layer: reactive-group-reactive with full reactive supportYou can also reference already-defined layers in a group:
(tp-define-layer 'existing-layer '(face bold))
(tp-define-layer-group 'my-group
'existing-layer ; Reference existing layer
'(face (:background "red") line-prefix ">>") ; Anonymous layer
'("named" . (face italic))) ; Named layerIf a layer group with the same name already exists, it will be overwritten. The first layer in the definition is the top layer (visible by default).
Examples:
;; Define status layers, then group them
(progn
(setq tp-layer-alist nil)
(setq tp-layer-groups nil)
(tp-define-layer 'highlight
'(face (:background "yellow" :foreground "black")))
(tp-define-layer 'error
'(face (:background "red" :foreground "white")))
(tp-define-layer 'info
'(face (:background "blue" :foreground "white")))
(tp-define-layer-group 'status-colors 'highlight 'error 'info)
(length (tp-group-props 'status-colors)))
;; => 3
;; Define a layer group with named layers
(progn
(setq tp-layer-alist nil)
(setq tp-layer-groups nil)
(tp-define-layer-group 'moon-phases
'("new" . (display "🌑"))
'("waxing-crescent" . (display "🌒"))
'("first-quarter" . (display "🌓"))
'("full" . (display "🌕")))
(tp-layer-props 'moon-phases-full))
;; => (display "🌕" tp-name moon-phases-full)define-tp and define-tp-group are convenience macros that provide a more concise syntax for tp-define-layer and tp-define-layer-group.
define-tp - Define Single Layer
Supports two formats:
Format 1 - Non-parameterized (empty arglist):
(define-tp tp-bold ()
'(face bold))Format 2 - Parameterized (with a single parameter):
(define-tp tp-space (pixel)
`(display (space :width (,pixel))))Usage with tp-set:
For non-parameterized layers, use t as the value:
;; Entire string
(tp-set "emacs" 'tp-bold t)
;; => #("emacs" 0 5 (tp-name tp-bold face bold))
;; Region (with plist-like format)
(tp-set 0 5 '(tp-bold t) "emacs")
;; => #("emacs" 0 5 (tp-name tp-bold face bold))For parameterized layers, pass the argument value:
;; Entire string
(tp-set "emacs" 'tp-space 2)
;; => #("emacs" 0 5 (tp-name tp-space display (space :width (2))))
;; Region (with plist-like format)
(tp-set 0 5 '(tp-space 2) "emacs")
;; => #("emacs" 0 5 (tp-name tp-space display (space :width (2))))define-tp-group - Define Layer Group
Convenience macro wrapping tp-define-layer-group:
(define-tp-group tp-moon-phases
'(display "🌑")
'(display "🌕"))(tp-layer-props LAYER-NAME)
(tp-group-props GROUP-NAME)Get properties for a layer or all layers in a group.
Examples:
;; Get layer properties
(progn
(setq tp-layer-alist nil)
(tp-define-layer 'my-layer '(face bold help-echo "tip"))
(tp-layer-props 'my-layer))
;; => (face bold help-echo "tip" tp-name my-layer)
;; Get group properties
(progn
(setq tp-layer-alist nil)
(setq tp-layer-groups nil)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer 'layer2 '(face italic))
(tp-define-layer-group 'my-group 'layer1 'layer2)
(length (tp-group-props 'my-group)))
;; => 2(tp-undefine-layer NAME)
(tp-undefine-group NAME)Remove layer or group definition.
Examples:
;; Undefine a layer
(progn
(setq tp-layer-alist nil)
(tp-define-layer 'temp-layer '(face bold))
(tp-undefine-layer 'temp-layer)
(tp-layer-props 'temp-layer))
;; => nil
;; Undefine a group
(progn
(setq tp-layer-alist nil)
(setq tp-layer-groups nil)
(tp-define-layer 'l1 '(face bold))
(tp-define-layer-group 'my-group 'l1)
(tp-undefine-group 'my-group)
(assoc 'my-group tp-layer-groups))
;; => nil(tp-layer-reset)Clear all layer and group definitions, including all reactive dependencies and watchers.
Examples:
(progn
(tp-define-layer 'test-layer '(face bold))
(tp-layer-reset)
(list tp-layer-alist tp-layer-groups))
;; => (nil nil)(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")
(tp-define-layer '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") tp-name reactive-layer);; Buffer/string region
(tp-put-layer START END LAYER IDX OBJECT)
;; Entire string
(tp-put-layer STRING LAYER IDX)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
Examples:
;; Put base layer at top
(progn
(tp-layer-reset)
(tp-define-layer 'base '(face default))
(tp-define-layer '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)
(tp-define-layer 'base '(face default))
(tp-define-layer '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)
(tp-define-layer 'base '(face default))
(tp-define-layer '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;; Buffer/string region
(tp-push-layer START END LAYER OBJECT)
;; Entire string
(tp-push-layer STRING LAYER)Push a layer to the top of the stack (equivalent to tp-put-layer ... 0).
Examples:
;; Push base layer first
(progn
(tp-layer-reset)
(tp-define-layer 'base '(face default))
(tp-define-layer '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)
(tp-define-layer 'base '(face default))
(tp-define-layer '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;; 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)
(tp-define-layer 'highlight '(face (:background "yellow")))
(tp-define-layer '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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; 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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; 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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer 'layer2 '(face italic))
(tp-define-layer '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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; 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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer 'layer2 '(face italic))
(tp-define-layer '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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; Buffer/string region
(tp-rotate-layer START END OBJECT)
;; Entire string
(tp-rotate-layer STRING)Rotate layers - top goes to bottom, next becomes visible.
Examples:
;; Stack: highlight (top) -> base (bottom)
(progn
(tp-layer-reset)
(tp-define-layer 'base '(face default))
(tp-define-layer '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;; Buffer/string region
(tp-pin-layer START END IDX/LAYER-NAME OBJECT)
;; Entire string
(tp-pin-layer STRING IDX/LAYER-NAME)Move a specific layer to the top (make it visible).
Examples:
;; Make 'base the top layer
(progn
(tp-layer-reset)
(tp-define-layer 'base '(face default))
(tp-define-layer '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;; 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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; 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.
Examples:
;; Merge layer1 and layer2 into merged-layer
(progn
(tp-layer-reset)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; 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.
Examples:
;; Flatten all layers into 'flat-layer
(progn
(tp-layer-reset)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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)
(tp-define-layer '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(tp-layer-list START END &optional OBJECT)Get list of all layer names in region.
Examples:
(progn
(tp-layer-reset)
(tp-define-layer 'highlight '(face (:background "yellow")))
(tp-define-layer '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 START END &optional OBJECT)Count layers in region.
Examples:
(progn
(tp-layer-reset)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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 START END NAME &optional OBJECT)Check if layer exists in region.
Examples:
(progn
(tp-layer-reset)
(tp-define-layer '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 START END &optional OBJECT)Get name of the top (visible) layer.
Examples:
(progn
(tp-layer-reset)
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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;; 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.
- Returns the modified string or nil for buffer operations.
Examples:
(progn
(tp-layer-reset)
(tp-define-layer 'layer1 '(face (:foreground "red")))
(tp-define-layer '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;; 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.
- Returns the modified string or nil for buffer operations.
Examples:
(let ((str (copy-sequence "Hello World")))
(tp-define-layer 'layer1 '(face bold))
(tp-define-layer '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 START END &optional OBJECT)Get all text property intervals from START to END in OBJECT.
- Returns a list of (START END PROPERTIES) for each interval.
- 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)) (6 11 (face italic)))(tp-intervals-map FUNCTION START END &optional OBJECT)Apply FUNCTION to all intervals between START and END in OBJECT.
- FUNCTION receives four arguments: interval-start, interval-end, top-props (visible layer properties), and below-props-lst (list of hidden layers).
- OBJECT can be a buffer or string; nil defaults to current buffer.
- Returns list of function results (nil values 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) (6 11 italic))(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)
(tp-define-layer '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)));; 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 plist containing all properties found in the range.
- 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))
;; => (face bold help-echo "Tip" face italic)(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
(let ((str (copy-sequence "text")))
(tp-set str 'face 'bold)
(tp-empty-p str)) ; => nil;; Complete example that can be run in a buffer
(progn
(tp-layer-reset)
;; Define layers for different highlighting purposes
(tp-define-layer 'code-base
'(face font-lock-keyword-face))
(tp-define-layer 'code-error
'(face (:underline (:color "red" :style wave))
help-echo "Syntax error"))
(tp-define-layer '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));; Complete example with layer group
(progn
(tp-layer-reset)
;; Define status layers as a group
(tp-define-layer 'status-todo '(face (:foreground "gray")))
(tp-define-layer 'status-progress '(face (:foreground "yellow")))
(tp-define-layer 'status-done '(face (:foreground "green")))
(tp-define-layer-group '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)));; Define temporary highlight layer
(progn
(tp-layer-reset)
(tp-define-layer 'temp-highlight
'(face (:background "yellow")))
(tp-layer-props 'temp-highlight))
;; => (face (:background "yellow") tp-name temp-highlight)
;; 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))📖 For a comprehensive guide with detailed examples, see Reactive Text Properties Complete Guide
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.
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")
(tp-define-layer '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!-
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.
(defvar highlight-color "yellow")
(tp-define-layer '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!
)(defvar fg-color "white")
(defvar bg-color "black")
(tp-define-layer '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 backgroundThe :data keyword defines additional reactive variables that aren't directly used in :props but can trigger computed value updates or be watched:
(tp-define-layer '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:
(tp-define-layer '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"The :compute keyword creates derived values that are automatically recalculated when their dependencies change:
(tp-define-layer '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!The :watch keyword lets you execute callbacks when reactive variables change:
(tp-define-layer '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"You can use reactive variables even without tp-define-layer. 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")All text property APIs (tp-set, tp-match-set, tp-regexp-set, etc.) now accept layer names directly:
(tp-define-layer '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)Layer groups can also use reactive features:
(tp-define-layer-group '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"))))To clear all reactive dependencies and watchers:
(tp-reactive-reset) ; Clears only reactive state
(tp-layer-reset) ; Clears layers, groups, AND reactive state;; Define theme variables
(defvar theme-fg "white")
(defvar theme-bg "black")
(defvar theme-accent "cyan")
;; Define theme-aware layers
(tp-define-layer 'code-keyword
:props '(face (:foreground $theme-accent :weight bold)))
(tp-define-layer 'code-comment
:props '(face (:foreground "gray" :slant italic)))
(tp-define-layer 'code-string
:props '(face (:foreground "green")))
;; Apply layers to code
(tp-match-set '("defun" "defvar" "let" "if" "when") 'code-keyword)
(tp-regexp-set ";.*$" 'code-comment)
(tp-regexp-set "\"[^\"]*\"" 'code-string)
;; Switch to light theme - just change 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"))GNU General Public License v2 or later.
Contributions are welcome! Please feel free to submit issues or pull requests.
tp.el - Making text properties powerful and easy to use