diff --git a/docs/reactive-text-properties.md b/docs/reactive-text-properties.md index 4a11c0a..6af20e0 100644 --- a/docs/reactive-text-properties.md +++ b/docs/reactive-text-properties.md @@ -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)。* diff --git a/tp.el b/tp.el index fcd5aca..69b262b 100644 --- a/tp.el +++ b/tp.el @@ -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