Replace the legacy managed layer renderer with one independent retained/reactive text runtime. TP now owns exact dependencies, stable objects, marker-backed mounts, property contribution composition, atomic publication, rollback, and direct text-property facades without ECSS or Ebox dependencies.\n\nBREAKING CHANGE: remove tp-render, tp-stack, scan-driven managed layers, inline runtime metadata, TP-owned CSS cascade APIs, and dollar-variable declarations.\n\nVerified: 290/290 ERT, shuffled 290/290 (seed 20260806), 8/8 doctests, WERROR compile-all, checkdoc, package-lint, diff-check, and isolated TP-only load.
511 lines
17 KiB
Markdown
511 lines
17 KiB
Markdown
# tp.el Complete Guide to Reactive Text Properties
|
|
|
|
> **Historical TP 0.3 document; obsolete for TP 1.0.** This document records the removed `$variable`, `tp-text`, inline `tp-name`, and scan-driven reactive-update model and remains only as migration and design history. None of the APIs, examples, or “current behavior” claims below describe TP 1.0. The current reactive model uses signals, bindings, `tp-computed`, `tp-watch`, and retained surfaces; see the [README](../README.md), [current architecture](ARCHITECTURE.md), and [API contract](API-SEMANTICS.md).
|
|
|
|
> Bringing modern frontend framework reactive programming paradigms to the Emacs text properties world
|
|
|
|
## Introduction
|
|
|
|
In traditional Emacs development, managing text properties has always been a tedious task. Whenever you want to change a property value, you need to manually find all related text regions and update them one by one. This approach is not only error-prone but also difficult to maintain.
|
|
|
|
**Reactive Text Properties** is one of the most innovative features in the tp.el library. It borrows the reactive programming concepts from modern frontend frameworks like Vue.js and React, allowing Emacs text properties to **automatically respond to variable changes**.
|
|
|
|
Imagine: you define the relationship between a variable and a property once, and from then on, whenever you change the variable's value, all text regions using that variable will **automatically update**. This is the magic of reactive text properties!
|
|
|
|
## From Traditional to Reactive
|
|
|
|
### Pain Points of the Traditional Approach
|
|
|
|
Let's first look at how the traditional approach handles dynamic text properties:
|
|
|
|
```lisp
|
|
;; Traditional approach: define a color variable
|
|
(defvar my-color "red")
|
|
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Hello World")
|
|
(tp-set 1 12 `(face (:foreground ,my-color)))
|
|
|
|
;; Here comes the problem: when you want to change the color...
|
|
(setq my-color "blue")
|
|
;; The text doesn't update automatically! You must manually reapply:
|
|
(tp-set 1 12 `(face (:foreground ,my-color))))
|
|
```
|
|
|
|
The problems with this approach are obvious:
|
|
1. **Manual tracking**: You need to remember which text regions use which variables
|
|
2. **Easy to miss**: In complex applications, it's easy to forget to update some regions
|
|
3. **Code redundancy**: Update logic is scattered throughout the code
|
|
|
|
### The Elegance of Reactive Approach
|
|
|
|
Now let's see how the reactive approach solves these problems:
|
|
|
|
```lisp
|
|
;; Reactive approach: define a color variable
|
|
(defvar my-color "red")
|
|
|
|
;; Define a reactive layer using $my-color to reference the variable
|
|
(define-tp my-highlight ()
|
|
'(face (:foreground $my-color)))
|
|
|
|
;; Apply to text
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Hello World")
|
|
(tp-set 1 12 'my-highlight)
|
|
|
|
;; Now, just change the variable!
|
|
(setq my-color "blue")
|
|
;; Magic happens: the text automatically turns blue!
|
|
)
|
|
```
|
|
|
|
Isn't that amazing? Let's dive deep into how this powerful feature works.
|
|
|
|
## Core Concepts
|
|
|
|
### Reactive Variables
|
|
|
|
In tp.el, any symbol starting with `$` is treated as a **reactive variable**. For example:
|
|
- `$my-color` → references variable `my-color`
|
|
- `$font-size` → references variable `font-size`
|
|
- `$theme-background` → references variable `theme-background`
|
|
|
|
When you use these `$`-prefixed symbols in property definitions, tp.el will:
|
|
1. Automatically resolve the variable's current value
|
|
2. Register a watcher to monitor variable changes
|
|
3. When the variable changes, automatically update all related text regions
|
|
|
|
## Basic Usage
|
|
|
|
### Your First Reactive Layer
|
|
|
|
Let's start with a simple example:
|
|
|
|
```lisp
|
|
;; Define a global variable
|
|
(defvar highlight-bg "yellow")
|
|
|
|
;; Define a reactive layer
|
|
(define-tp simple-highlight ()
|
|
'(face (:background $highlight-bg)))
|
|
|
|
;; Create a test buffer and apply the layer
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "This is text that needs highlighting")
|
|
(tp-set 1 (point-max) 'simple-highlight)
|
|
;; => "Initial background color: yellow"
|
|
;; Change the variable
|
|
(setq highlight-bg "cyan")
|
|
;; => "Updated background color: cyan"
|
|
)
|
|
```
|
|
|
|
### Multiple Reactive Variables
|
|
|
|
A layer can reference multiple reactive variables:
|
|
|
|
```lisp
|
|
;; Define multiple variables
|
|
(defvar fg-color "white")
|
|
(defvar bg-color "darkGreen")
|
|
(defvar underline-color "red")
|
|
|
|
;; Define a layer using multiple variables
|
|
(define-tp multi-var-layer ()
|
|
'(face ( :foreground $fg-color
|
|
:background $bg-color
|
|
:underline (:color $underline-color))))
|
|
|
|
;; Test
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Multi-variable reactive example")
|
|
(tp-set 1 (point-max) 'multi-var-layer)
|
|
|
|
;; Changing any variable triggers an update
|
|
(setq fg-color "yellow") ; Foreground turns yellow
|
|
(setq bg-color "navy") ; Background turns navy
|
|
(setq underline-color "lime") ; Underline turns lime green
|
|
)
|
|
```
|
|
|
|
## Advanced Features: :data, :compute, and :watch
|
|
|
|
tp.el's reactive system borrows from Vue's API, providing three powerful keywords:
|
|
|
|
### :data - Define Additional Reactive State
|
|
|
|
Sometimes you need reactive variables that aren't directly used in `:props`. This is where `:data` comes in.
|
|
|
|
Main uses of `:data`:
|
|
1. Define auxiliary variables that don't appear directly in properties
|
|
2. Provide initial values for variables
|
|
3. Work together with `:compute`
|
|
|
|
### :compute - Computed Properties
|
|
|
|
`:compute` lets you define **derived values**—their values are computed from other variables:
|
|
|
|
```lisp
|
|
;; Complete computed properties example
|
|
(define-tp computed-greeting ()
|
|
:props '(display $full-greeting face (:foreground $status-color))
|
|
:data '((user-name . "John")
|
|
(greeting-prefix . "Hello"))
|
|
:compute '((full-greeting (lambda ()
|
|
(format "%s, %s! Welcome back."
|
|
greeting-prefix user-name)))
|
|
(status-color (lambda ()
|
|
(if (string= user-name "Admin")
|
|
"red"
|
|
"green")))))
|
|
|
|
;; Test
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Test text")
|
|
(tp-set 1 (point-max) 'computed-greeting)
|
|
;; Initial state
|
|
(message "full-greeting = %s" full-greeting)
|
|
;; => "Hello, John! Welcome back."
|
|
(message "status-color = %s" status-color)
|
|
;; => "green"
|
|
;; Change user-name
|
|
(setq user-name "Admin")
|
|
;; Computed properties update automatically!
|
|
(message "full-greeting = %s" full-greeting)
|
|
;; => "Hello, Admin! Welcome back."
|
|
(message "status-color = %s" status-color)
|
|
;; => "red"
|
|
;; Change greeting-prefix
|
|
(setq greeting-prefix "Hi")
|
|
(message "full-greeting = %s" full-greeting))
|
|
;; => "Hi, Admin! Welcome back."
|
|
```
|
|
|
|
### :watch - Watch Variable Changes
|
|
|
|
`:watch` lets you execute **side effect** operations when variables change:
|
|
|
|
```lisp
|
|
;; Layer with watchers
|
|
(define-tp watched-layer ()
|
|
:props '(face (:foreground $status-color))
|
|
:data '((status-color . "green"))
|
|
:watch '((status-color
|
|
(lambda (new-val old-val layer-name)
|
|
(message "[%s] Color changed from %s to %s"
|
|
layer-name old-val new-val)))))
|
|
|
|
;; Test
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Test text")
|
|
(tp-set 1 (point-max) 'watched-layer)
|
|
|
|
;; Change color - triggers watcher
|
|
(setq status-color "yellow")
|
|
;; Message: "[watched-layer] Color changed from green to yellow"
|
|
|
|
(setq status-color "red"))
|
|
;; Message: "[watched-layer] Color changed from yellow to red"
|
|
```
|
|
|
|
Typical uses for `:watch`:
|
|
- Logging
|
|
- Updating external state
|
|
- Triggering notifications
|
|
- Performing cleanup operations
|
|
|
|
## Complete Practical Examples
|
|
|
|
### Example 1: Dynamic Color Status Indicator
|
|
|
|
This example shows how to create an indicator that automatically changes color based on status:
|
|
|
|
```lisp
|
|
(tp-layer-reset)
|
|
|
|
;; Define status color variables
|
|
(defvar status-color "gray")
|
|
(defvar status-text "Not Started")
|
|
|
|
;; Define status indicator layer
|
|
(define-tp status-indicator ()
|
|
'(face (:background $status-color) display $status-text))
|
|
|
|
;; Define status update function
|
|
(defun set-status (status)
|
|
"Set status, automatically update color and text"
|
|
(pcase status
|
|
('pending (setq status-color "gray" status-text "Pending"))
|
|
('running (setq status-color "blue" status-text "Running"))
|
|
('success (setq status-color "green" status-text "Success"))
|
|
('warning (setq status-color "orange" status-text "Warning"))
|
|
('error (setq status-color "red" status-text "Error"))))
|
|
|
|
;; Test the status indicator
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Status")
|
|
(tp-set 1 (point-max) 'status-indicator)
|
|
|
|
;; Simulate status changes
|
|
(set-status 'pending)
|
|
(message "Status: %s, Color: %s" status-text status-color)
|
|
;; => "Status: Pending, Color: gray"
|
|
|
|
(set-status 'running)
|
|
(message "Status: %s, Color: %s" status-text status-color)
|
|
;; => "Status: Running, Color: blue"
|
|
|
|
(set-status 'success)
|
|
(message "Status: %s, Color: %s" status-text status-color))
|
|
;; => "Status: Success, Color: green"
|
|
```
|
|
|
|
### Example 2: Theme Switching System
|
|
|
|
This example shows how to create a switchable theme system:
|
|
|
|
```lisp
|
|
(tp-layer-reset)
|
|
|
|
;; Define theme color variables
|
|
(defvar keyword-color nil)
|
|
(defvar string-color nil)
|
|
|
|
;; Define theme-related reactive layers
|
|
(define-tp themed-keyword ()
|
|
'(face (:foreground $keyword-color :weight bold)))
|
|
|
|
(define-tp themed-string ()
|
|
'(face (:foreground $string-color)))
|
|
|
|
;; Define theme switching functions
|
|
(defun switch-to-dark-theme ()
|
|
"Switch to dark theme"
|
|
(interactive)
|
|
(setq keyword-color "light blue"
|
|
string-color "green")
|
|
(message "Switched to dark theme"))
|
|
|
|
(defun switch-to-light-theme ()
|
|
"Switch to light theme"
|
|
(interactive)
|
|
(setq keyword-color "blue"
|
|
string-color "dark green")
|
|
(message "Switched to light theme"))
|
|
|
|
;; Test theme switching
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "(defun hello () \"greeting\")")
|
|
|
|
;; Apply different theme layers
|
|
(tp-match-set "defun" 'themed-keyword)
|
|
(tp-regexp-set "\".+\"" 'themed-string)
|
|
|
|
(switch-to-dark-theme)
|
|
;; Initially using dark theme
|
|
(message "Keyword color: %s" keyword-color)
|
|
(message "String color: %s" string-color)
|
|
|
|
;; Switch to light theme
|
|
(switch-to-light-theme)
|
|
;; Text updates automatically!
|
|
(message "Keyword color: %s" keyword-color)
|
|
(message "String color: %s" string-color))
|
|
```
|
|
|
|
## Anonymous Reactive Layers
|
|
|
|
Besides using `define-tp` to define named layers, you can also use reactive variables directly in property lists. tp.el will automatically generate unique names for these anonymous layers:
|
|
|
|
```lisp
|
|
(tp-layer-reset)
|
|
|
|
(defvar inline-color "purple")
|
|
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Anonymous reactive layer example")
|
|
|
|
;; Use $inline-color directly, no need to pre-define a layer
|
|
(tp-set 1 (point-max) '(face (:foreground $inline-color)))
|
|
|
|
;; Text is now purple
|
|
(message "Color: %s" (plist-get (tp-at 1 'face) :foreground))
|
|
;; => "purple"
|
|
|
|
;; Change the variable
|
|
(setq inline-color "orange")
|
|
|
|
;; Text automatically turns orange
|
|
(message "Color: %s" (plist-get (tp-at 1 'face) :foreground)))
|
|
;; => "orange"
|
|
```
|
|
|
|
Anonymous reactive layers are suitable for simple scenarios where you don't need to reuse the same layer definition in multiple places.
|
|
|
|
## Reactive Text (tp-text)
|
|
|
|
Besides reactive text **properties**, tp.el also supports reactive **text content** itself. Through the special `tp-text` property, you can make the text content reactive too—when the bound variable changes, the text content automatically updates.
|
|
|
|
### Basic Usage
|
|
|
|
The `tp-text` property has two ways to use:
|
|
|
|
#### 1. Initialize with Current Text
|
|
|
|
When `tp-text` is `nil`, it will be automatically set to the current region's text content:
|
|
|
|
```lisp
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Hello World")
|
|
;; When tp-text is nil, auto-initialize to current text "Hello"
|
|
(tp-set 1 6 '(face bold tp-text nil))
|
|
;; Now tp-text value is "Hello"
|
|
(message "tp-text = %s" (tp-at 1 'tp-text)))
|
|
;; => "Hello"
|
|
```
|
|
|
|
#### 2. Replace Text Content
|
|
|
|
When `tp-text` is a string, it replaces the text in the region while preserving other text properties:
|
|
|
|
```lisp
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "Hello World")
|
|
;; When tp-text is a string, replace the text content
|
|
(tp-set 1 6 '(face bold tp-text "Hi"))
|
|
;; Text becomes "Hi World", and "Hi" still has bold style
|
|
(message "buffer = %s" (buffer-string)))
|
|
;; => "Hi World"
|
|
```
|
|
|
|
### Reactive Text Layers
|
|
|
|
The real power of `tp-text` comes from combining it with reactive variables:
|
|
|
|
```lisp
|
|
;; Define a reactive variable
|
|
(defvar my-dynamic-text "Loading...")
|
|
|
|
;; Define a layer containing tp-text
|
|
(define-tp dynamic-content ()
|
|
:props '(face (:foreground "blue") tp-text $my-dynamic-text))
|
|
|
|
;; Apply to text
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "placeholder")
|
|
(tp-set 1 12 'dynamic-content)
|
|
;; Text now shows "Loading..."
|
|
(message "Initial text: %s" (buffer-string))
|
|
;; => "Loading..."
|
|
|
|
;; Change the variable
|
|
(setq my-dynamic-text "Data loaded successfully!")
|
|
;; Text updates automatically!
|
|
(message "After update: %s" (buffer-string)))
|
|
;; => "Data loaded successfully!"
|
|
```
|
|
|
|
### Using :compute for Dynamic Text
|
|
|
|
`tp-text` can be combined with `:compute` to create dynamic text derived from other variables:
|
|
|
|
```lisp
|
|
(define-tp greeting-layer ()
|
|
:props '(face (:foreground "green") tp-text $full-greeting)
|
|
:data '((user-name . "Guest")
|
|
(greeting-prefix . "Welcome"))
|
|
:compute '((full-greeting
|
|
(lambda ()
|
|
(format "%s, %s!" greeting-prefix user-name)))))
|
|
|
|
;; Apply to text
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "placeholder")
|
|
(tp-set 1 12 'greeting-layer)
|
|
;; Shows "Welcome, Guest!"
|
|
(message "Initial: %s" (buffer-string))
|
|
|
|
;; Change user name
|
|
(setq user-name "John")
|
|
;; Text automatically updates to "Welcome, John!"
|
|
(message "After update: %s" (buffer-string)))
|
|
```
|
|
|
|
### Anonymous Reactive Text
|
|
|
|
You can also use reactive `tp-text` directly in property lists without defining a layer:
|
|
|
|
```lisp
|
|
(defvar inline-text "Original content")
|
|
|
|
(tp-pop-to-buffer "*tp-test*"
|
|
(insert "placeholder")
|
|
;; Directly use reactive tp-text
|
|
(tp-set 1 12 '(face bold tp-text $inline-text))
|
|
;; Shows "Original content"
|
|
|
|
;; Change the variable
|
|
(setq inline-text "New content")
|
|
;; Text automatically updates to "New content"
|
|
)
|
|
```
|
|
|
|
### Important Notes
|
|
|
|
1. **tp-text on strings returns a new string**: Emacs strings cannot change length in place, so string-object calls return a new string instead of modifying the original. A sub-region `tp-text` replaces only that region and keeps the rest of the string; the whole-string form returns just the replacement text.
|
|
2. **Preserves existing properties**: When using `tp-set` or `tp-add` to set `tp-text`, existing text properties are preserved.
|
|
3. **Non-reactive properties don't add tp-name**: If there are no reactive variables (`$` prefix) in the text properties, `tp-name` and other reactive-specific properties won't be added, maintaining native text property behavior.
|
|
|
|
## Value Transformation with :transform
|
|
|
|
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:
|
|
|
|
```lisp
|
|
;; 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.90
|
|
|
|
;; 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-text` string value
|
|
- Returns the transformed string for display
|
|
- Is applied both on initial display and reactive updates
|
|
- Must return a string; errors and non-string results propagate rather
|
|
than leaving stale output visible
|
|
|
|
> 📖 **For more optimization features like batched updates and debug mode, see [Reactive System Optimization](reactive-optimization-en.md)**
|
|
|
|
## Summary
|
|
|
|
tp.el's reactive text properties feature brings a modern reactive programming experience to Emacs development. By using `$`-prefixed reactive variables, `:data` to define state, `:compute` for derived values, `:watch` to monitor changes, and `:transform` for value formatting, you can build a more dynamic and maintainable text property system.
|
|
|
|
Key points:
|
|
1. **Reactive Variables**: Use `$` prefix to reference variables
|
|
2. **:props**: Define properties containing reactive variables
|
|
3. **:data**: Define additional reactive state and initial values
|
|
4. **:compute**: Define computed properties derived from other variables
|
|
5. **:watch**: Watch variable changes and execute side effects
|
|
6. **:transform**: Transform tp-text values before display
|
|
7. **Automatic Updates**: Change variable values, all related text updates automatically
|
|
8. **Reactive Text (tp-text)**: Make text content itself reactive
|