This commit is contained in:
Kinneyzhang 2026-07-26 16:31:36 +08:00
parent 29b2ee54d3
commit af0980efef
4 changed files with 719 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,212 @@
# tp.el 响应式系统优化文档
本文档基于 [twidget](https://github.com/Kinneyzhang/twidget.git) 项目的实践经验,对 tp.el 的响应式系统进行了优化和增强。
## 优化建议评估
以下是针对 tp.el 响应式系统的六项优化建议的评估和实现情况:
### 1. 细粒度响应式更新Granular Reactive Updates
**建议**:支持区域内的部分更新,只更新响应式部分,保留周围文本属性。
**评估**已经实现。tp.el 通过 `tp-intervals-map` 和基于区间的更新机制,已经支持细粒度的属性更新。更新只影响具有特定 `tp-name` 的区域。
### 2. 响应式符号清理Reactive Symbol Cleanup✅ 已实现
**建议**:当 widget 销毁时,添加注销响应式符号的机制。
**评估**:已经实现。`tp--unregister-reactive-deps` 函数负责清理:
- 当层被重新定义时自动调用
- 当层被取消定义(`tp-undefine-layer`)时自动调用
- 清理变量监听器、计算属性和数据变量
**关键函数**
- `tp--unregister-reactive-deps`
- `tp--unregister-layer-watchers`
- `tp--unregister-layer-computed`
- `tp--unregister-layer-data`
### 3. 作用域响应式Scoped Reactivity✅ 已实现
**建议**:为响应式变量添加实例/上下文作用域。
**评估**:已经实现。`where` 参数在以下函数中支持缓冲区局部更新:
- `tp--update-layer-regions`
- `tp--update-reactive-text`
当使用 `setq-local` 时,更新只影响特定缓冲区。
### 4. 批量更新Batched Updates🆕 新增
**建议**:当多个响应式值同时变化时,批量处理更新以避免冗余的缓冲区修改。
**实现**:新增 `tp-with-batch-updates` 宏:
```elisp
;; 使用批量更新
(tp-with-batch-updates
(setq my-color "red")
(setq my-size 14)
(setq my-text "Hello"))
;; 所有更新在批量结束后一次性应用到缓冲区
```
**关键函数和变量**
- `tp-with-batch-updates` - 批量更新宏
- `tp--batch-update-active` - 标记是否在批量更新中
- `tp--batch-update-pending` - 待处理的更新列表
- `tp--flush-batch-updates` - 应用所有待处理更新
### 5. 值转换Value Transformation🆕 新增
**建议**:允许注册转换函数,在 tp-text 更新时运行。
**实现**:新增 `:transform` 选项:
```elisp
;; 定义带转换的层
(define-tp currency-display ()
:props '(face bold tp-text $amount)
:data '((amount . "100"))
:transform (lambda (text)
(format "$%s.00" text)))
;; 使用后100 会显示为 $100.00
```
**关键函数和变量**
- `tp-layer-transforms` - 存储层转换函数
- 转换在 `tp--handle-tp-text-property``tp--update-reactive-text` 中应用
### 6. 调试模式Debug Mode🆕 新增
**建议**:添加调试模式以追踪响应式更新。
**实现**:新增调试功能:
```elisp
;; 启用调试模式
(setq tp-debug-mode t)
;; 同时在 minibuffer 显示调试信息
(setq tp-debug-echo t)
;; 查看调试日志
(tp-debug-show)
;; 清除调试日志
(tp-debug-clear)
```
**关键函数和变量**
- `tp-debug-mode` - 启用/禁用调试模式
- `tp-debug-echo` - 是否在 minibuffer 显示调试信息
- `tp-debug-log` - 记录调试信息
- `tp-debug-show` - 显示调试缓冲区
- `tp-debug-clear` - 清除调试日志
调试日志包含:
- 变量变化通知(旧值 → 新值)
- 层更新追踪
- 批量更新开始/结束
- 转换应用信息
## 新增功能详解
### 批量更新 (tp-with-batch-updates)
当需要同时修改多个响应式变量时,使用批量更新可以避免多次缓冲区更新:
```elisp
(define-tp themed-text ()
:props '(face (:foreground $fg-color :background $bg-color))
:data '((fg-color . "white") (bg-color . "black")))
(with-temp-buffer
(insert "Hello World")
(tp-set 1 12 'themed-text)
;; 不使用批量更新:每个 setq 都会触发一次缓冲区更新
(setq fg-color "yellow") ; 第一次更新
(setq bg-color "navy") ; 第二次更新
;; 使用批量更新:所有变化在结束时一次性应用
(tp-with-batch-updates
(setq fg-color "red")
(setq bg-color "blue"))) ; 只更新一次
```
### 值转换 (:transform)
转换函数允许在显示前处理 tp-text 的值:
```elisp
;; 数字格式化
(define-tp price-display ()
:props '(tp-text $price)
:data '((price . "99.9"))
:transform (lambda (text)
(format "$%.2f" (string-to-number text))))
;; 日期格式化
(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)))))
;; 大写转换
(define-tp uppercase-text ()
:props '(tp-text $content)
:data '((content . "hello"))
:transform #'upcase)
```
### 调试模式
调试模式帮助开发者理解响应式更新流程:
```elisp
;; 启用完整调试
(setq tp-debug-mode t)
(setq tp-debug-echo t)
;; 定义和使用响应式层
(define-tp test-layer ()
:props '(face (:foreground $my-color))
:data '((my-color . "red")))
(with-temp-buffer
(insert "Test")
(tp-set 1 5 'test-layer)
(setq my-color "blue"))
;; 调试输出示例:
;; [12:34:56.789] Variable my-color changed: "red" -> "blue" (where: global)
;; [12:34:56.790] Updating layer test-layer (tp-text affected: no)
```
## 架构说明
这些优化遵循 tp.el 的分层架构原则:
1. **调试模式** - 作为基础工具层功能
2. **批量更新** - 在响应式系统层实现
3. **值转换** - 在层定义和响应式文本处理中实现
所有新功能都与现有的响应式系统无缝集成,不破坏现有 API。
## 相关函数一览
| 函数/变量 | 描述 |
|----------|------|
| `tp-debug-mode` | 启用调试模式 |
| `tp-debug-echo` | 启用 minibuffer 调试输出 |
| `tp-debug-log` | 记录调试信息 |
| `tp-debug-show` | 显示调试缓冲区 |
| `tp-debug-clear` | 清除调试日志 |
| `tp-with-batch-updates` | 批量更新宏 |
| `tp-layer-transforms` | 层转换函数存储 |
| `:transform` | 层定义中的转换选项 |

View File

@ -0,0 +1,507 @@
# tp.el Complete Guide to Reactive Text Properties
> 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 only affects buffer text**: For string objects, since Emacs string length is fixed, `tp-text` won't replace string content.
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.00
;; Date formatting
(define-tp date-display ()
:props '(tp-text $timestamp)
:data '((timestamp . "1703865600"))
:transform (lambda (text)
(format-time-string "%Y-%m-%d"
(seconds-to-time (string-to-number text)))))
;; Uppercase conversion
(define-tp uppercase-text ()
:props '(tp-text $content)
:data '((content . "hello"))
:transform #'upcase)
;; "hello" displays as "HELLO"
```
The transform function:
- Receives the raw `tp-text` string value
- Returns the transformed string for display
- Is applied both on initial display and reactive updates
- Errors in transform functions are caught and logged
> 📖 **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