improve code

This commit is contained in:
Kinneyzhang 2025-12-25 01:30:13 +08:00
parent bd6110d182
commit c5325844eb
2 changed files with 87 additions and 325 deletions

View File

@ -16,19 +16,18 @@
让我们先看看传统方式如何处理动态文本属性:
```elisp
```lisp
;; 传统方式:定义一个颜色变量
(defvar my-color "red")
;; 应用到文本
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "Hello World")
(tp-set 1 10 `(face (:foreground ,my-color)))
(tp-set 1 12 `(face (:foreground ,my-color)))
;; 问题来了:当你想改变颜色时...
(setq my-color "blue")
;; 文本不会自动更新!你必须手动重新应用:
(tp-set 1 10 `(face (:foreground ,my-color)))) ; 手动更新
(tp-set 1 12 `(face (:foreground ,my-color))))
```
这种方式的问题显而易见:
@ -40,24 +39,23 @@
现在让我们看看响应式方式如何解决这些问题:
```elisp
```lisp
;; 响应式方式:定义一个颜色变量
(defvar my-color "red")
;; 定义一个响应式层,使用 $my-color 引用变量
(tp-define-layer 'my-highlight
:props '(face (:foreground $my-color)))
'(face (:foreground $my-color)))
;; 应用到文本
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "Hello World")
(tp-push-layer 1 10 'my-highlight)
(tp-set 1 12 'my-highlight)
;; 现在,只需改变变量!
(setq my-color "blue")
;; 神奇的事情发生了:文本自动变成蓝色!
(tp-at 1 'face)) ; => (:foreground "blue")
)
```
是不是很神奇?让我们深入了解这个强大功能的工作原理。
@ -76,123 +74,66 @@
2. 注册一个监听器,监视变量的变化
3. 当变量改变时,自动更新所有相关的文本区域
### 工作流程图
```
┌─────────────────────────────────────────────────────────────┐
│ 定义响应式层 │
│ (tp-define-layer 'my-layer │
│ :props '(face (:foreground $my-color))) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ tp.el 自动注册变量监听器 │
│ add-variable-watcher → my-color │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 应用层到文本 │
│ (tp-push-layer 1 10 'my-layer) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 变量改变时... │
│ (setq my-color "blue") │
│ ↓ │
│ 监听器触发 → 更新层定义 → 更新所有应用该层的文本区域 │
└─────────────────────────────────────────────────────────────┘
```
## 基础用法
### 第一个响应式层
让我们从一个简单的例子开始:
```elisp
;; 首先,重置所有层定义(确保干净的环境)
(tp-layer-reset)
```lisp
;; 定义一个全局变量
(defvar highlight-bg "yellow")
;; 定义响应式层
(tp-define-layer 'simple-highlight
:props '(face (:background $highlight-bg)))
'(face (:background $highlight-bg)))
;; 创建测试缓冲区并应用层
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "这是一段需要高亮的文本")
(tp-push-layer 1 (point-max) 'simple-highlight)
;; 检查当前属性
(message "初始背景色: %s" (plist-get (tp-at 1 'face) :background))
(tp-set 1 (point-max) 'simple-highlight)
;; => "初始背景色: yellow"
;; 改变变量
(setq highlight-bg "cyan")
;; 再次检查属性
(message "更新后背景色: %s" (plist-get (tp-at 1 'face) :background)))
;; => "更新后背景色: cyan"
)
```
### 多个响应式变量
一个层可以引用多个响应式变量:
```elisp
(tp-layer-reset)
```lisp
;; 定义多个变量
(defvar fg-color "white")
(defvar bg-color "black")
(defvar bg-color "darkGreen")
(defvar underline-color "red")
;; 定义使用多个变量的层
(tp-define-layer 'multi-var-layer
:props '(face (:foreground $fg-color
:background $bg-color
:underline (:color $underline-color :style wave))))
'(face ( :foreground $fg-color
:background $bg-color
:underline (:color $underline-color))))
;; 测试
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "多变量响应式示例")
(tp-push-layer 1 (point-max) 'multi-var-layer)
(tp-set 1 (point-max) 'multi-var-layer)
;; 改变任何一个变量都会触发更新
(setq fg-color "yellow") ; 前景色变黄
(setq bg-color "navy") ; 背景色变海军蓝
(setq underline-color "lime") ; 下划线变酸橙绿
(tp-at 1 'face))
;; => (:foreground "yellow" :background "navy" :underline (:color "lime" :style wave))
)
```
## 进阶功能::data、:compute 和 :watch
tp.el 的响应式系统借鉴了 Vue 3 Composition API提供了三个强大的关键字
tp.el 的响应式系统借鉴了 Vue 的 API提供了三个强大的关键字
### :data - 定义额外的响应式状态
有时候你需要一些响应式变量,但它们不直接用于 `:props` 中。这时可以使用 `:data`
```elisp
(tp-layer-reset)
;; 使用 :data 定义额外的响应式变量
(tp-define-layer 'greeting-layer
:props '(help-echo $greeting-text)
:data '((user-name . "张三") ; 带初始值的变量
(greeting-prefix . "你好")))
;; 此时 user-name = "张三", greeting-prefix = "你好"
;; 但 greeting-text 还没有定义!
;; 检查变量是否已定义
(message "user-name = %s" user-name) ; => "张三"
(message "greeting-prefix = %s" greeting-prefix) ; => "你好"
```
有时候你需要一些响应式变量,但它们不直接用于 `:props` 中。这时可以使用 `:data`
`:data` 的主要用途:
1. 定义不直接出现在属性中的辅助变量
@ -203,57 +144,47 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
`:compute` 让你可以定义**派生值**——它们的值由其他变量计算得出:
```elisp
(tp-layer-reset)
```lisp
;; 完整的计算属性示例
(tp-define-layer 'computed-greeting
:props '(help-echo $full-greeting face (:foreground $status-color))
:props '(display $full-greeting face (:foreground $status-color))
:data '((user-name . "张三")
(greeting-prefix . "你好"))
:compute '((full-greeting (lambda ()
(format "%s, %s欢迎回来。"
greeting-prefix user-name)))
(status-color (lambda ()
(if (string= user-name "管理员")
"red"
"green")))))
(if (string= user-name "管理员")
"red"
"green")))))
;; 测试
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "测试文本")
(tp-push-layer 1 (point-max) 'computed-greeting)
(tp-set 1 (point-max) 'computed-greeting)
;; 初始状态
(message "full-greeting = %s" full-greeting)
;; => "你好, 张三!欢迎回来。"
(message "status-color = %s" status-color)
;; => "green"
;; 改变 user-name
(setq user-name "管理员")
;; 计算属性自动更新!
(message "full-greeting = %s" full-greeting)
;; => "你好, 管理员!欢迎回来。"
(message "status-color = %s" status-color)
;; => "red"
;; 改变 greeting-prefix
(setq greeting-prefix "您好")
(message "full-greeting = %s" full-greeting))
;; => "您好, 管理员!欢迎回来。"
;; => "您好, 管理员!欢迎回来。"
```
### :watch - 监听变量变化
`:watch` 让你可以在变量改变时执行**副作用**操作:
```elisp
(tp-layer-reset)
```lisp
;; 带监听器的层
(tp-define-layer 'watched-layer
:props '(face (:foreground $status-color))
@ -264,16 +195,16 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
layer-name old-val new-val)))))
;; 测试
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "测试文本")
(tp-push-layer 1 (point-max) 'watched-layer)
(tp-set 1 (point-max) 'watched-layer)
;; 改变颜色 - 触发监听器
(setq status-color "yellow")
;; 消息: "【watched-layer】颜色从 green 变为 yellow"
(setq status-color "red"))
;; 消息: "【watched-layer】颜色从 yellow 变为 red"
;; 消息: "【watched-layer】颜色从 yellow 变为 red"
```
`:watch` 的典型用途:
@ -288,7 +219,7 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
这个示例展示如何创建一个根据状态自动变色的指示器:
```elisp
```lisp
(tp-layer-reset)
;; 定义状态颜色变量
@ -297,8 +228,7 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
;; 定义状态指示器层
(tp-define-layer 'status-indicator
:props '(face (:foreground "white" :background $status-color)
help-echo $status-text))
'(face (:background $status-color) display $status-text))
;; 定义状态更新函数
(defun set-status (status)
@ -311,9 +241,9 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
('error (setq status-color "red" status-text "错误"))))
;; 测试状态指示器
(with-temp-buffer
(insert "[状态]")
(tp-push-layer 1 5 'status-indicator)
(tp-pop-to-buffer "*tp-test*"
(insert "状态")
(tp-set 1 (point-max) 'status-indicator)
;; 模拟状态变化
(set-status 'pending)
@ -326,133 +256,72 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
(set-status 'success)
(message "状态: %s, 颜色: %s" status-text status-color))
;; => "状态: 成功, 颜色: green"
;; => "状态: 成功, 颜色: green"
```
### 示例二:主题切换系统
这个示例展示如何创建一个可切换的主题系统:
```elisp
```lisp
(tp-layer-reset)
;; 定义主题颜色变量
(defvar keyword-color "cyan")
(defvar string-color "green")
(defvar comment-color "gray")
(defvar keyword-color nil)
(defvar string-color nil)
;; 定义主题相关的响应式层
(tp-define-layer 'themed-keyword
:props '(face (:foreground $keyword-color :weight bold)))
'(face (:foreground $keyword-color :weight bold)))
(tp-define-layer 'themed-string
:props '(face (:foreground $string-color)))
(tp-define-layer 'themed-comment
:props '(face (:foreground $comment-color :slant italic)))
'(face (:foreground $string-color)))
;; 定义主题切换函数
(defun switch-to-dark-theme ()
"切换到深色主题"
(interactive)
(setq keyword-color "cyan"
string-color "green"
comment-color "gray")
(setq keyword-color "light blue"
string-color "green")
(message "已切换到深色主题"))
(defun switch-to-light-theme ()
"切换到浅色主题"
(interactive)
(setq keyword-color "blue"
string-color "dark green"
comment-color "dark gray")
string-color "dark green")
(message "已切换到浅色主题"))
;; 测试主题切换
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "(defun hello () \"greeting\")")
;; 应用不同的主题层
(tp-push-layer 2 6 'themed-keyword) ; defun
(tp-push-layer 14 24 'themed-string) ; "greeting"
(tp-match-set "defun" 'themed-keyword)
(tp-regexp-set "\".+\"" 'themed-string)
(switch-to-dark-theme)
;; 初始是深色主题
(message "关键字颜色: %s" keyword-color) ; => "cyan"
(message "字符串颜色: %s" string-color) ; => "green"
(message "关键字颜色: %s" keyword-color)
(message "字符串颜色: %s" string-color)
;; 切换到浅色主题
(switch-to-light-theme)
;; 文本自动更新!
(message "关键字颜色: %s" keyword-color) ; => "blue"
(message "字符串颜色: %s" string-color)) ; => "dark green"
```
### 示例三:输入字段高亮
这个示例展示如何根据输入状态动态改变文本外观:
```elisp
(tp-layer-reset)
;; 定义状态变量
(defvar field-color "gray")
(defvar field-message "请输入内容")
;; 定义表单字段层
(tp-define-layer 'form-field
:props '(face (:foreground $field-color)
help-echo $field-message)
:watch '((field-color
(lambda (new old layer)
(message "字段颜色变化: %s -> %s" old new)))))
;; 验证函数
(defun validate-input (value min-length)
"验证输入并更新状态"
(cond
((string= value "")
(setq field-color "gray"
field-message "请输入内容"))
((< (length value) min-length)
(setq field-color "red"
field-message (format "至少需要 %d 个字符" min-length)))
(t
(setq field-color "green"
field-message "✓ 验证通过"))))
;; 测试表单验证
(with-temp-buffer
(insert "用户名: [ ]")
(tp-push-layer 9 19 'form-field)
;; 空值状态
(validate-input "" 3)
(message "消息: %s, 颜色: %s" field-message field-color)
;; => "消息: 请输入内容, 颜色: gray"
;; 输入太短
(validate-input "ab" 3)
;; => 触发: "字段颜色变化: gray -> red"
(message "消息: %s, 颜色: %s" field-message field-color)
;; => "消息: 至少需要 3 个字符, 颜色: red"
;; 输入足够
(validate-input "abc" 3)
;; => 触发: "字段颜色变化: red -> green"
(message "消息: %s, 颜色: %s" field-message field-color))
;; => "消息: ✓ 验证通过, 颜色: green"
(message "关键字颜色: %s" keyword-color)
(message "字符串颜色: %s" string-color))
```
## 匿名响应式层
除了使用 `tp-define-layer` 定义命名层你还可以直接在属性列表中使用响应式变量。tp.el 会自动为这些匿名层生成唯一的名称:
```elisp
```lisp
(tp-layer-reset)
(defvar inline-color "purple")
(with-temp-buffer
(tp-pop-to-buffer "*tp-test*"
(insert "匿名响应式层示例")
;; 直接使用 $inline-color无需预先定义层
@ -467,126 +336,11 @@ tp.el 的响应式系统借鉴了 Vue 3 的 Composition API提供了三个强
;; 文本自动变成橙色
(message "颜色: %s" (plist-get (tp-at 1 'face) :foreground)))
;; => "orange"
;; => "orange"
```
匿名响应式层适用于简单的场景,当你不需要在多个地方复用同一个层定义时。
## 响应式层组
层组也可以包含响应式层。这对于定义一组相关的、可以统一管理的响应式层非常有用:
```elisp
(tp-layer-reset)
;; 定义响应式层组
(tp-define-layer-group 'notification-styles
;; 成功通知
'("success" :props (face (:foreground $success-fg :background $success-bg))
:data ((success-fg . "white") (success-bg . "green")))
;; 警告通知
'("warning" :props (face (:foreground $warning-fg :background $warning-bg))
:data ((warning-fg . "black") (warning-bg . "yellow")))
;; 错误通知
'("error" :props (face (:foreground $error-fg :background $error-bg))
:data ((error-fg . "white") (error-bg . "red"))))
;; 使用层组
(with-temp-buffer
(insert "操作成功! 请注意! 出错了!")
(tp-push-layer 1 7 'notification-styles-success)
(tp-push-layer 9 14 'notification-styles-warning)
(tp-push-layer 16 21 'notification-styles-error)
;; 改变成功通知的颜色
(setq success-bg "dark green")
;; 只有成功通知的背景色会改变
(message "成功背景: %s" (plist-get (tp-at 1 'face) :background)))
;; => "dark green"
```
## 清理和重置
### 重置响应式状态
当你想清除所有响应式依赖但保留层定义时:
```elisp
(tp-reactive-reset)
```
这会:
- 移除所有变量监听器
- 清除依赖关系
- 保留层定义
### 完全重置
当你想清除所有层定义和响应式状态时:
```elisp
(tp-layer-reset)
```
这会:
- 移除所有层定义
- 移除所有层组定义
- 清除所有响应式状态
## 最佳实践
### 1. 合理命名变量
为响应式变量使用清晰、有意义的名称:
```elisp
;; 好的命名
(tp-define-layer 'header
:props '(face (:foreground $header-text-color
:background $header-bg-color)))
;; 不好的命名
(tp-define-layer 'header
:props '(face (:foreground $c1 :background $c2)))
```
### 2. 使用 :data 提供初始值
始终为变量提供合理的初始值:
```elisp
(tp-define-layer 'status-indicator
:props '(face (:foreground $status-color))
:data '((status-color . "gray"))) ; 初始为灰色
```
### 3. 将相关变量组织在一起
使用一致的命名前缀来组织相关变量:
```elisp
(tp-define-layer 'code-block
:props '(face (:foreground $code-fg
:background $code-bg))
:data '((code-fg . "white")
(code-bg . "dark gray")))
```
### 4. 使用 :watch 进行调试
在开发过程中,使用 `:watch` 来追踪变量变化:
```elisp
(tp-define-layer 'debug-layer
:props '(face (:foreground $debug-color))
:data '((debug-color . "red"))
:watch '((debug-color
(lambda (new old layer)
(message "[DEBUG] %s.debug-color: %s -> %s"
layer old new)))))
```
## 总结
tp.el 的响应式文本属性功能为 Emacs 开发带来了现代化的响应式编程体验。通过使用 `$` 前缀的响应式变量、`:data` 定义状态、`:compute` 计算派生值、`:watch` 监听变化,你可以构建出更加动态、易于维护的文本属性系统。
@ -598,9 +352,3 @@ tp.el 的响应式文本属性功能为 Emacs 开发带来了现代化的响应
4. **:compute**:定义由其他变量派生的计算属性
5. **:watch**:监听变量变化并执行副作用
6. **自动更新**:改变变量值,所有相关文本自动更新
开始使用响应式文本属性,让你的 Emacs 插件开发更加优雅、高效!
---
*本文档是 tp.el 项目的一部分。更多信息请参阅 [项目主页](../README.md) 和 [完整中文文档](../README_CN.md)。*

36
tp.el
View File

@ -393,6 +393,12 @@ Also adds variable watchers so changes to data vars trigger computed updates."
"Unregister data variables for LAYER-NAME."
(setq tp-layer-data (assq-delete-all layer-name tp-layer-data)))
(defmacro tp-with-current-buffer (buffer-or-name &rest body)
(declare (indent defun))
`(with-current-buffer ,buffer-or-name
(let ((inhibit-read-only t))
,@body)))
(defun tp--ensure-reactive-variables (var-symbols)
"Ensure all VAR-SYMBOLS are defined as global variables.
VAR-SYMBOLS can be a list of symbols or cons cells (SYMBOL . INITIAL-VALUE).
@ -431,13 +437,13 @@ WHERE specifies which buffers to update:
nil)))
(if (and where (bufferp where) (buffer-live-p where))
;; setq-local case: only update the specific buffer
(with-current-buffer where
(tp-with-current-buffer where
(save-excursion
(tp-search-map apply-props-fn 'tp-name layer-name)))
;; setq case: update all buffers that have the text property
(dolist (buf (buffer-list))
(when (buffer-live-p buf)
(with-current-buffer buf
(tp-with-current-buffer buf
(save-excursion
(tp-search-map apply-props-fn 'tp-name layer-name))))))))))
@ -1182,7 +1188,7 @@ Returns modified object or list of regions."
;; Buffer or nil (current buffer)
(t
(let ((buf (or object (current-buffer))))
(with-current-buffer buf
(tp-with-current-buffer buf
(save-excursion
(goto-char (point-min))
(let (regions)
@ -1233,7 +1239,7 @@ Returns modified object or list of regions."
;; Buffer or nil (current buffer)
(t
(let ((buf (or object (current-buffer))))
(with-current-buffer buf
(tp-with-current-buffer buf
(save-excursion
(goto-char (point-min))
(let (regions)
@ -1414,7 +1420,7 @@ Uses `tp-search-forward' for buffers and `tp-search' for strings."
(t
(let ((result nil)
(buf (or object (current-buffer))))
(with-current-buffer buf
(tp-with-current-buffer buf
(dotimes (_ count)
(setq result (tp-search-forward property value t))))
result)))))
@ -1441,7 +1447,7 @@ Uses `tp-search-backward' for buffers and `tp-search' for strings."
(t
(let ((result nil)
(buf (or object (current-buffer))))
(with-current-buffer buf
(tp-with-current-buffer buf
(dotimes (_ count)
(setq result (tp-search-backward property value))))
result)))))
@ -1476,7 +1482,7 @@ Returns the number of successful matches."
(t
(let* ((buf (or object (current-buffer)))
(matches 0))
(with-current-buffer buf
(tp-with-current-buffer buf
(let ((search-start (or start (point-min)))
(search-end (or end (point-max))))
(save-excursion
@ -1598,7 +1604,7 @@ Returns the number of successful matches."
(t
(let* ((buf (or object (current-buffer)))
(matches 0))
(with-current-buffer buf
(tp-with-current-buffer buf
(let ((search-start (or start (point-min)))
(search-end (or end (point-max))))
(save-excursion
@ -1759,7 +1765,7 @@ Each element contains the start position, end position, and property value."
pos property obj end)
end)))))
;; Buffer object
(with-current-buffer obj
(tp-with-current-buffer obj
(while (< pos end)
(let* ((props (text-properties-at pos))
(has-prop (plist-member props property))
@ -1921,7 +1927,7 @@ Uses `object-intervals' (Emacs 28.1+)."
((stringp object)
(object-intervals (substring object start end)))
((bufferp object)
(with-current-buffer (get-buffer-create object)
(tp-with-current-buffer (get-buffer-create object)
(object-intervals (buffer-substring start end))))
(t (error "Invalid format of object: %S"
(type-of object))))))
@ -1935,7 +1941,7 @@ Uses `object-intervals' (Emacs 28.1+)."
((stringp obj)
(null (object-intervals obj)))
((bufferp obj)
(with-current-buffer obj
(tp-with-current-buffer obj
(null (object-intervals (buffer-substring (point-min) (point-max))))))
(t (error "Invalid object type: %S" (type-of obj))))))
@ -3243,6 +3249,14 @@ Returns the modified object (string) or nil for buffer operations."
(tp-add-to-layers all-indices start end plist obj))))
(if (stringp obj) obj nil)))
(defmacro tp-pop-to-buffer (buffer-or-name &rest body)
(declare (indent defun))
`(let ((buffer (get-buffer-create ,buffer-or-name)))
(tp-with-current-buffer buffer
(erase-buffer)
,@body
(read-only-mode 1))
(pop-to-buffer buffer)))
(provide 'tp)
;;; tp.el ends here