tp/docs/reactive-text-properties-en.md
Kinneyzhang 0d35358e05 refactor(tp)!: implement retained reactive runtime
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.
2026-08-07 00:39:50 +08:00

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