From 36c0a60f3f8a42992587ef63cd6ab1035624ff35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 09:30:17 +0000 Subject: [PATCH] refactor: redesign :compute to compute reactive variable values, add :data support Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com> --- tp-tests.el | 215 +++++++++++++++++++++++++-------------- tp.el | 282 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 319 insertions(+), 178 deletions(-) diff --git a/tp-tests.el b/tp-tests.el index ae47ad1..9c795b3 100644 --- a/tp-tests.el +++ b/tp-tests.el @@ -2337,7 +2337,7 @@ Returns list of (START END VALUE) intervals." (makunbound 'tp-test-regexp-color)))) ;;; ============================================================ -;;; :watch and :compute Tests (Vue 3 style reactivity) +;;; :watch, :data, and :compute Tests (Vue 3 style reactivity) ;;; ============================================================ (ert-deftest tp-test-define-layer-with-watch () @@ -2374,59 +2374,86 @@ Returns list of (START END VALUE) intervals." (makunbound 'tp-test-watch-var) (makunbound 'tp-test-watch-log)))) -(ert-deftest tp-test-define-layer-with-compute () - "Test tp-define-layer with :compute for derived properties." +(ert-deftest tp-test-define-layer-with-data () + "Test tp-define-layer with :data for additional reactive variables." (tp-test-with-temp-buffer - (defvar tp-test-compute-size nil "Test variable for compute.") - (setq tp-test-compute-size 10) (unwind-protect (progn + (tp-define-layer test-data-layer + :props (face (:foreground $tp-test-data-color)) + :data (tp-test-data-extra)) + ;; Check that variables were auto-defined + (should (boundp 'tp-test-data-color)) + (should (boundp 'tp-test-data-extra)) + ;; Check data is registered + (should (assoc 'test-data-layer tp-layer-data)) + ;; Check the layer is defined + (should (assoc 'test-data-layer tp-layer-alist))) + ;; Cleanup + (makunbound 'tp-test-data-color) + (makunbound 'tp-test-data-extra)))) + +(ert-deftest tp-test-define-layer-with-compute () + "Test tp-define-layer with :compute for computed reactive variables." + (tp-test-with-temp-buffer + (unwind-protect + (progn + ;; Set up the source variables + (setq tp-test-first-name "John") + (setq tp-test-last-name "Doe") (tp-define-layer test-compute-layer - :props (face (:foreground $tp-test-compute-size)) - :compute (help-echo (lambda () (format "Size: %d" tp-test-compute-size)))) + :props (help-echo $tp-test-full-name) + :data (tp-test-first-name tp-test-last-name) + :compute ((tp-test-full-name + (lambda () + (concat tp-test-first-name " " tp-test-last-name))))) ;; Check the layer is defined (should (assoc 'test-compute-layer tp-layer-alist)) ;; Check the computed is registered (should (assoc 'test-compute-layer tp-layer-computed)) - ;; Check initial computed value + ;; Check the computed variable has initial value + (should (equal tp-test-full-name "John Doe")) + ;; Check the layer property uses the computed value (let ((props (cdr (assoc 'test-compute-layer tp-layer-alist)))) - (should (equal (plist-get props 'help-echo) "Size: 10")))) + (should (equal (plist-get props 'help-echo) "John Doe")))) ;; Cleanup - (makunbound 'tp-test-compute-size)))) + (makunbound 'tp-test-first-name) + (makunbound 'tp-test-last-name) + (makunbound 'tp-test-full-name)))) -(ert-deftest tp-test-define-layer-with-watch-and-compute () - "Test tp-define-layer with both :watch and :compute." +(ert-deftest tp-test-define-layer-with-data-and-compute () + "Test tp-define-layer with :data and :compute together." (tp-test-with-temp-buffer - (defvar tp-test-wc-color nil "Test variable for watch and compute.") - (defvar tp-test-wc-log nil "Log of watch callback invocations.") - (setq tp-test-wc-color "blue") - (setq tp-test-wc-log nil) (unwind-protect (progn - (tp-define-layer test-wc-layer - :props (face (:foreground $tp-test-wc-color)) - :watch ((tp-test-wc-color - (lambda (new old layer) - (push (list new old) tp-test-wc-log)))) - :compute (help-echo (lambda () (concat "Color: " tp-test-wc-color)))) - ;; Check layer is defined - (should (assoc 'test-wc-layer tp-layer-alist)) - ;; Check both watcher and computed are registered - (should (assoc 'test-wc-layer tp-layer-watchers)) - (should (assoc 'test-wc-layer tp-layer-computed)) - ;; Check initial computed value - (let ((props (cdr (assoc 'test-wc-layer tp-layer-alist)))) - (should (equal (plist-get props 'help-echo) "Color: blue"))) - ;; Change the variable - (setq tp-test-wc-color "green") - ;; Check the watcher was called - (should (= (length tp-test-wc-log) 1)) - (let ((log-entry (car tp-test-wc-log))) - (should (equal (nth 0 log-entry) "green")) - (should (equal (nth 1 log-entry) "blue")))) + (tp-define-layer test-dc-layer + :props (face (:foreground $tp-test-dc-color) help-echo $tp-test-dc-full-name) + :data (tp-test-dc-first tp-test-dc-last) + :compute ((tp-test-dc-full-name + (lambda () + (concat tp-test-dc-first " " tp-test-dc-last))))) + ;; Set data values + (setq tp-test-dc-color "blue") + (setq tp-test-dc-first "Jane") + (setq tp-test-dc-last "Smith") + ;; Re-eval to trigger the compute + (tp-define-layer test-dc-layer + :props (face (:foreground $tp-test-dc-color) help-echo $tp-test-dc-full-name) + :data (tp-test-dc-first tp-test-dc-last) + :compute ((tp-test-dc-full-name + (lambda () + (concat tp-test-dc-first " " tp-test-dc-last))))) + ;; Check data is registered + (should (assoc 'test-dc-layer tp-layer-data)) + ;; Check computed is registered + (should (assoc 'test-dc-layer tp-layer-computed)) + ;; Check the computed value + (should (equal tp-test-dc-full-name "Jane Smith"))) ;; Cleanup - (makunbound 'tp-test-wc-color) - (makunbound 'tp-test-wc-log)))) + (makunbound 'tp-test-dc-color) + (makunbound 'tp-test-dc-first) + (makunbound 'tp-test-dc-last) + (makunbound 'tp-test-dc-full-name)))) (ert-deftest tp-test-define-layer-watch-requires-props () "Test that :watch requires :props to be explicitly specified." @@ -2442,29 +2469,43 @@ Returns list of (START END VALUE) intervals." (should-error (macroexpand-1 '(tp-define-layer test-invalid - :compute (help-echo (lambda () "computed"))))))) + :compute ((some-var (lambda () "computed")))))))) -(ert-deftest tp-test-undefine-layer-clears-watch-and-compute () - "Test tp-undefine-layer clears watchers and computed properties." +(ert-deftest tp-test-define-layer-data-requires-props () + "Test that :data requires :props to be explicitly specified." + (tp-test-with-temp-buffer + (should-error + (macroexpand-1 + '(tp-define-layer test-invalid + :data (some-var)))))) + +(ert-deftest tp-test-undefine-layer-clears-watch-compute-data () + "Test tp-undefine-layer clears watchers, computed, and data." (tp-test-with-temp-buffer - (defvar tp-test-undef-wc-var nil "Test variable for undefine wc.") - (setq tp-test-undef-wc-var "test") (unwind-protect (progn - (tp-define-layer test-undef-wc - :props (face (:foreground $tp-test-undef-wc-var)) - :watch ((tp-test-undef-wc-var (lambda (n o l) nil))) - :compute (help-echo (lambda () "computed"))) + (tp-define-layer test-undef-wcd + :props (face (:foreground $tp-test-undef-color) help-echo $tp-test-undef-full) + :data (tp-test-undef-first tp-test-undef-last) + :watch ((tp-test-undef-color (lambda (n o l) nil))) + :compute ((tp-test-undef-full + (lambda () + (concat tp-test-undef-first " " tp-test-undef-last))))) ;; Check registrations - (should (assoc 'test-undef-wc tp-layer-watchers)) - (should (assoc 'test-undef-wc tp-layer-computed)) + (should (assoc 'test-undef-wcd tp-layer-watchers)) + (should (assoc 'test-undef-wcd tp-layer-computed)) + (should (assoc 'test-undef-wcd tp-layer-data)) ;; Undefine the layer - (tp-undefine-layer 'test-undef-wc) - ;; Check watchers and computed are cleaned up - (should-not (assoc 'test-undef-wc tp-layer-watchers)) - (should-not (assoc 'test-undef-wc tp-layer-computed))) + (tp-undefine-layer 'test-undef-wcd) + ;; Check all are cleaned up + (should-not (assoc 'test-undef-wcd tp-layer-watchers)) + (should-not (assoc 'test-undef-wcd tp-layer-computed)) + (should-not (assoc 'test-undef-wcd tp-layer-data))) ;; Cleanup - (makunbound 'tp-test-undef-wc-var)))) + (makunbound 'tp-test-undef-color) + (makunbound 'tp-test-undef-first) + (makunbound 'tp-test-undef-last) + (makunbound 'tp-test-undef-full)))) (ert-deftest tp-test-define-layer-group-with-watch () "Test tp-define-layer-group with :watch (format-4)." @@ -2498,45 +2539,75 @@ Returns list of (START END VALUE) intervals." (ert-deftest tp-test-define-layer-group-with-compute () "Test tp-define-layer-group with :compute (format-4)." (tp-test-with-temp-buffer - (defvar tp-test-group-compute-var nil "Test variable for group compute.") - (setq tp-test-group-compute-var 42) (unwind-protect (progn + (setq tp-test-group-first "Group") + (setq tp-test-group-last "Test") (tp-define-layer-group test-compute-group - ;; Using $tp-test-group-compute-var to trigger reactive path - ("computed" :props (face (:foreground $tp-test-group-compute-var)) - :compute (help-echo (lambda () (format "Value: %s" tp-test-group-compute-var)))) + ("computed" :props (help-echo $tp-test-group-full) + :data (tp-test-group-first tp-test-group-last) + :compute ((tp-test-group-full + (lambda () + (concat tp-test-group-first " " tp-test-group-last))))) ("static" :props (face (:foreground "blue")))) ;; Check the group is defined (should (assoc 'test-compute-group tp-layer-groups)) ;; Check the computed layer has its compute registered (should (assoc 'test-compute-group-computed tp-layer-computed)) ;; Static layer should not have computed - (should-not (assoc 'test-compute-group-static tp-layer-computed))) + (should-not (assoc 'test-compute-group-static tp-layer-computed)) + ;; Check computed value + (should (equal tp-test-group-full "Group Test"))) ;; Cleanup - (makunbound 'tp-test-group-compute-var)))) + (makunbound 'tp-test-group-first) + (makunbound 'tp-test-group-last) + (makunbound 'tp-test-group-full)))) -(ert-deftest tp-test-reactive-reset-clears-watch-and-compute () - "Test tp-reactive-reset clears watchers and computed properties." +(ert-deftest tp-test-reactive-reset-clears-all () + "Test tp-reactive-reset clears watchers, computed, and data." (tp-test-with-temp-buffer - (defvar tp-test-reset-wc-var nil "Test variable for reset wc.") - (setq tp-test-reset-wc-var "test") (unwind-protect (progn - (tp-define-layer test-reset-wc - :props (face (:foreground $tp-test-reset-wc-var)) - :watch ((tp-test-reset-wc-var (lambda (n o l) nil))) - :compute (help-echo (lambda () "computed"))) + (tp-define-layer test-reset-all + :props (face (:foreground $tp-test-reset-color) help-echo $tp-test-reset-full) + :data (tp-test-reset-first tp-test-reset-last) + :watch ((tp-test-reset-color (lambda (n o l) nil))) + :compute ((tp-test-reset-full + (lambda () + (concat tp-test-reset-first " " tp-test-reset-last))))) ;; Check registrations (should tp-layer-watchers) (should tp-layer-computed) + (should tp-layer-data) ;; Reset reactive (tp-reactive-reset) ;; Check all are cleared (should-not tp-layer-watchers) - (should-not tp-layer-computed)) + (should-not tp-layer-computed) + (should-not tp-layer-data)) + ;; Cleanup - variables may or may not be bound + (ignore-errors (makunbound 'tp-test-reset-color)) + (ignore-errors (makunbound 'tp-test-reset-first)) + (ignore-errors (makunbound 'tp-test-reset-last)) + (ignore-errors (makunbound 'tp-test-reset-full))))) + +(ert-deftest tp-test-auto-define-variables () + "Test that reactive variables are auto-defined when not bound." + (tp-test-with-temp-buffer + (unwind-protect + (progn + ;; Variables should not exist before + (should-not (boundp 'tp-test-auto-var1)) + (should-not (boundp 'tp-test-auto-var2)) + (tp-define-layer test-auto-layer + :props (face (:foreground $tp-test-auto-var1)) + :data (tp-test-auto-var2)) + ;; Variables should now exist + (should (boundp 'tp-test-auto-var1)) + (should (boundp 'tp-test-auto-var2))) ;; Cleanup - (makunbound 'tp-test-reset-wc-var)))) + (makunbound 'tp-test-auto-var1) + (makunbound 'tp-test-auto-var2)))) (provide 'tp-ert-tests) ;;; tp-ert-tests.el ends here diff --git a/tp.el b/tp.el index a2ead44..d84754a 100644 --- a/tp.el +++ b/tp.el @@ -65,10 +65,15 @@ is a list of (VAR-SYMBOL . CALLBACK) pairs. CALLBACK receives (NEW-VAL OLD-VAL LAYER-NAME) when VAR-SYMBOL changes.") (defvar tp-layer-computed nil - "Alist mapping layer names to their computed property definitions. -Each element is (LAYER-NAME . COMPUTED-PLIST) where COMPUTED-PLIST -is a plist of (PROP-KEY . COMPUTE-FN) pairs. -COMPUTE-FN is evaluated to get the current value of the property.") + "Alist mapping layer names to their computed variable definitions. +Each element is (LAYER-NAME . COMPUTED-LIST) where COMPUTED-LIST +is a list of (VAR-SYMBOL . COMPUTE-FN) pairs. +COMPUTE-FN is evaluated to get the current value of the reactive variable.") + +(defvar tp-layer-data nil + "Alist mapping layer names to their data variable definitions. +Each element is (LAYER-NAME . VAR-LIST) where VAR-LIST is a list +of variable symbols defined via :data.") (defvar tp--anonymous-layer-counter 0 "Counter for generating unique anonymous layer names.") @@ -196,9 +201,10 @@ Only the reactive portions of the properties are stored for each variable." ;; Clean up empty dependency entries (setq tp-reactive-deps (cl-remove-if (lambda (dep) (null (cdr dep))) tp-reactive-deps)) - ;; Also clean up layer watchers and computed properties + ;; Also clean up layer watchers, computed properties, and data (tp--unregister-layer-watchers layer-name) - (tp--unregister-layer-computed layer-name)) + (tp--unregister-layer-computed layer-name) + (tp--unregister-layer-data layer-name)) (defun tp--reactive-variable-watcher (symbol newval operation _where) "Watcher function called when a reactive variable changes. @@ -254,24 +260,26 @@ NEWVAL is the new value, OLDVAL is the old value." layer-name watch-sym err)))))))) (defun tp--update-layer-computed (layer-name override-alist) - "Update computed properties for LAYER-NAME with OVERRIDE-ALIST. -Evaluates compute functions and updates the layer properties." + "Update computed reactive variables for LAYER-NAME with OVERRIDE-ALIST. +Evaluates compute functions and updates the reactive variable values. +Returns an updated override-alist with the new computed values." (when-let ((computed (cdr (assoc layer-name tp-layer-computed)))) - (let ((current-props (cdr (assoc layer-name tp-layer-alist)))) - (when current-props - (cl-loop for (prop-key compute-fn) on computed by #'cddr - do (let ((computed-val - (condition-case err - ;; Resolve reactive symbols in compute function result - (tp--resolve-reactive-symbols - (funcall compute-fn) override-alist) - (error - (message "tp: compute error for %s.%s: %s" - layer-name prop-key err) - nil)))) - (when computed-val - (setq current-props (plist-put current-props prop-key computed-val))))) - (tp--set-layer-props layer-name current-props))))) + (dolist (comp computed) + (let* ((var-sym (car comp)) + (compute-fn (cdr comp)) + (computed-val + (condition-case err + (funcall compute-fn) + (error + (message "tp: compute error for %s.%s: %s" + layer-name var-sym err) + nil)))) + (when computed-val + ;; Update the global variable + (set var-sym computed-val) + ;; Add to override-alist for property resolution + (push (cons var-sym computed-val) override-alist))))) + override-alist) (defun tp--register-layer-watchers (layer-name watchers) "Register WATCHERS for LAYER-NAME. @@ -286,12 +294,16 @@ WATCHERS is a list of (VAR-SYMBOL CALLBACK) pairs." (push (cons layer-name watcher-pairs) tp-layer-watchers))))) (defun tp--register-layer-computed (layer-name computed) - "Register COMPUTED property definitions for LAYER-NAME. -COMPUTED is a plist of (PROP-KEY COMPUTE-FN) pairs." + "Register COMPUTED variable definitions for LAYER-NAME. +COMPUTED is a list of (VAR-SYMBOL COMPUTE-FN) pairs." (when computed - (if (assoc layer-name tp-layer-computed) - (setf (cdr (assoc layer-name tp-layer-computed)) computed) - (push (cons layer-name computed) tp-layer-computed)))) + (let ((computed-pairs + (mapcar (lambda (comp) + (cons (car comp) (cadr comp))) + computed))) + (if (assoc layer-name tp-layer-computed) + (setf (cdr (assoc layer-name tp-layer-computed)) computed-pairs) + (push (cons layer-name computed-pairs) tp-layer-computed))))) (defun tp--unregister-layer-watchers (layer-name) "Unregister all watchers for LAYER-NAME." @@ -301,15 +313,42 @@ COMPUTED is a plist of (PROP-KEY COMPUTE-FN) pairs." "Unregister all computed properties for LAYER-NAME." (setq tp-layer-computed (assq-delete-all layer-name tp-layer-computed))) -(defun tp--apply-initial-computed (resolved-props compute) - "Apply initial computed values to RESOLVED-PROPS using COMPUTE definitions. -COMPUTE is a plist of (PROP-KEY COMPUTE-FN) pairs. -Returns the modified RESOLVED-PROPS." - (cl-loop for (prop-key compute-fn) on compute by #'cddr - do (let ((val (tp--resolve-reactive-symbols (funcall compute-fn) nil))) - (when val - (setq resolved-props (plist-put resolved-props prop-key val))))) - resolved-props) +(defun tp--apply-initial-computed (compute) + "Apply initial computed values using COMPUTE definitions. +COMPUTE is a list of (VAR-SYMBOL COMPUTE-FN) pairs. +Sets the global variables to their computed values." + (dolist (comp compute) + (let* ((var-sym (car comp)) + (compute-fn (cadr comp)) + (val (condition-case err + (funcall compute-fn) + (error + (message "tp: initial compute error for %s: %s" var-sym err) + nil)))) + (when val + (set var-sym val))))) + +(defun tp--register-layer-data (layer-name data-vars) + "Register DATA-VARS for LAYER-NAME. +DATA-VARS is a list of variable symbols defined via :data." + (when data-vars + (if (assoc layer-name tp-layer-data) + (setf (cdr (assoc layer-name tp-layer-data)) data-vars) + (push (cons layer-name data-vars) tp-layer-data)))) + +(defun tp--unregister-layer-data (layer-name) + "Unregister data variables for LAYER-NAME." + (setq tp-layer-data (assq-delete-all layer-name tp-layer-data))) + +(defun tp--ensure-reactive-variables (var-symbols) + "Ensure all VAR-SYMBOLS are defined as global variables. +If a variable is not bound, define it with nil as initial value." + (dolist (sym var-symbols) + (let ((var-sym (if (tp--reactive-symbol-p sym) + (tp--reactive-var-symbol sym) + sym))) + (unless (boundp var-sym) + (set var-sym nil))))) (defun tp--update-layer-regions (layer-name) "Update all text regions that have LAYER-NAME applied. @@ -340,7 +379,8 @@ Re-applies the layer properties using tp-search-map and tp-add." ;; Clear all registries (setq tp-reactive-deps nil) (setq tp-layer-watchers nil) - (setq tp-layer-computed nil)) + (setq tp-layer-computed nil) + (setq tp-layer-data nil)) ;;; Core Property Functions @@ -1865,38 +1905,39 @@ Returns a plist of all properties in the region or string." (defun tp--parse-define-layer-args (args) "Parse ARGS for tp-define-layer macro. -Returns a plist with keys :props, :watch, :compute. -When :watch or :compute are present, :props is required." - (let (props watch compute has-keywords) +Returns a plist with keys :props, :data, :watch, :compute. +When :watch, :compute, or :data are present, :props is required." + (let (props data watch compute has-keywords) (cond ;; Check for keyword arguments format ((and (keywordp (car args)) - (memq (car args) '(:props :watch :compute))) + (memq (car args) '(:props :data :watch :compute))) (setq has-keywords t) ;; Parse keyword arguments (let ((rest args)) (while rest (pcase (car rest) (:props (setq props (cadr rest) rest (cddr rest))) + (:data (setq data (cadr rest) rest (cddr rest))) (:watch (setq watch (cadr rest) rest (cddr rest))) (:compute (setq compute (cadr rest) rest (cddr rest))) (_ (error "Unknown keyword in tp-define-layer: %s" (car rest)))))) - ;; Validate: if :watch or :compute present, :props must be present - (when (and (or watch compute) (null props)) - (error "When using :watch or :compute, :props must be explicitly specified"))) + ;; Validate: if :watch, :compute, or :data present, :props must be present + (when (and (or watch compute data) (null props)) + (error "When using :watch, :compute, or :data, :props must be explicitly specified"))) ;; Format 1: single plist (legacy) ((and (= (length args) 1) (listp (car args))) (setq props (car args))) (t (error "Invalid tp-define-layer format"))) - (list :props props :watch watch :compute compute))) + (list :props props :data data :watch watch :compute compute))) (defmacro tp-define-layer (name &rest args) "Define a single text property layer named NAME. This macro supports three formats: -Format 1 - Direct plist (legacy, no :watch/:compute support): +Format 1 - Direct plist (legacy, no :watch/:compute/:data support): (tp-define-layer layer-name (display \"🌑\" face (:height 1.0))) @@ -1904,59 +1945,73 @@ Format 2 - With :props keyword: (tp-define-layer layer-name :props (display \"🌑\" face (:height 1.0))) -Format 3 - With :props, :watch, and/or :compute (Vue 3 style reactivity): +Format 3 - With :props, :data, :watch, and/or :compute (Vue 3 style reactivity): (tp-define-layer layer-name - :props (face (:foreground $my-color) help-echo \"tip\") + ;; props: $-prefixed symbols are reactive variables; auto-defined if not bound + :props (face (:foreground $my-color) help-echo $full-name) + ;; data: additional reactive variables not used in props; auto-defined if not bound + :data (first-name last-name) + ;; compute: list of (VAR-NAME FUNCTION) - compute reactive variable values + :compute ((full-name (lambda () (concat first-name \" \" last-name)))) + ;; watch: list of (VAR-NAME CALLBACK) - side effects when vars change :watch ((my-color (lambda (new old layer) - (message \"Color changed from %s to %s\" old new)))) - :compute (derived-prop (lambda () (concat \"Value: \" $my-var)))) + (message \"Color changed from %s to %s\" old new))))) Reactive Variables: - If any symbol in the property specification starts with $, it is - treated as a reactive variable. When that variable's value changes, - all text regions with this layer will be automatically updated. + If any symbol in :props starts with $, it is treated as a reactive variable. + Variables in :data are also reactive. All reactive variables are automatically + defined as global variables if they are not already bound. -:watch - A list of (VAR-SYMBOL CALLBACK) pairs. CALLBACK is called - when VAR-SYMBOL changes, receiving (NEW-VALUE OLD-VALUE LAYER-NAME). - Similar to Vue 3's watch() function for side effects. +:data - A list of variable symbols for additional reactive state not in :props. -:compute - A plist of (PROP-KEY COMPUTE-FN) pairs. COMPUTE-FN is a - function that returns the computed value for PROP-KEY. The function - can use reactive variables ($-prefixed symbols) and will be - re-evaluated when any of its reactive dependencies change. - Similar to Vue 3's computed() for derived state. +:compute - A list of (VAR-SYMBOL COMPUTE-FN) pairs. COMPUTE-FN is evaluated + to compute the value of VAR-SYMBOL. Can reference other reactive variables + from both :props and :data. -Note: When using :watch or :compute, you MUST use :props to specify +:watch - A list of (VAR-SYMBOL CALLBACK) pairs. CALLBACK is called when + VAR-SYMBOL changes, receiving (NEW-VALUE OLD-VALUE LAYER-NAME). + +Note: When using :watch, :compute, or :data, you MUST use :props to specify the text properties explicitly. -If a layer with the same NAME already exists, it will be overwritten -with the new definition. - +If a layer with the same NAME already exists, it will be overwritten. The layer is stored in `tp-layer-alist'." (declare (indent defun)) (let* ((parsed (tp--parse-define-layer-args args)) (properties (plist-get parsed :props)) + (data (plist-get parsed :data)) (watch (plist-get parsed :watch)) (compute (plist-get parsed :compute)) (reactive-syms (tp--collect-reactive-symbols properties)) - ;; Also collect reactive symbols from compute functions - (compute-reactive-syms - (when compute - (cl-loop for (_key fn) on compute by #'cddr - append (tp--collect-reactive-symbols fn)))) - (all-reactive-syms (delete-dups (append reactive-syms compute-reactive-syms)))) - (if all-reactive-syms - ;; Has reactive symbols - register dependencies and resolve at runtime + ;; Collect computed variable names (they become reactive too) + (computed-vars (when compute (mapcar #'car compute))) + ;; All variables that need to be reactive + (all-reactive-syms (delete-dups (append reactive-syms))) + ;; All variables to ensure are defined (including data) + (all-vars-to-define (delete-dups + (append (mapcar #'tp--reactive-var-symbol reactive-syms) + data + computed-vars)))) + (if (or all-reactive-syms data compute) + ;; Has reactive features - register dependencies and resolve at runtime `(progn + ;; Ensure all reactive variables are defined + (tp--ensure-reactive-variables ',all-vars-to-define) + ;; Register data variables + ,@(when data + `((tp--register-layer-data ',name ',data))) + ;; Register computed variable definitions + ,@(when compute + `((tp--register-layer-computed ',name ',compute) + ;; Apply initial computed values + (tp--apply-initial-computed ',compute))) + ;; Register reactive dependencies (tp--register-reactive-deps ',name ',all-reactive-syms ',properties) + ;; Register watchers ,@(when watch `((tp--register-layer-watchers ',name ',watch))) - ,@(when compute - `((tp--register-layer-computed ',name ',compute))) + ;; Set layer properties with resolved values (let ((resolved-props (tp--resolve-reactive-symbols ',properties))) - ;; Apply initial computed values - ,@(when compute - `((setq resolved-props (tp--apply-initial-computed resolved-props ',compute)))) (tp--set-layer-props ',name resolved-props)) (assoc ',name tp-layer-alist)) ;; No reactive symbols - use static properties @@ -1972,8 +2027,8 @@ Returns 'symbol, 'format-1, 'format-2, 'format-3, 'format-4, or nil if invalid." (cond ;; Symbol - reference to existing layer ((symbolp element) 'symbol) - ;; Format 4 - ("name" :props (plist...) [:watch ...] [:compute ...]) - ;; Named layer with :props and optional :watch/:compute + ;; Format 4 - ("name" :props (plist...) [:data ...] [:watch ...] [:compute ...]) + ;; Named layer with :props and optional :data/:watch/:compute ((and (listp element) (> (length element) 3) (stringp (car element)) @@ -2010,26 +2065,28 @@ IDX is the index for anonymous elements. Returns a cons cell (LAYER-NAME . PROPERTIES) or a symbol if ELEMENT references an already-defined layer. -For format-4 elements, returns (LAYER-NAME :props PROPS :watch WATCH :compute COMPUTE)." +For format-4 elements, returns (LAYER-NAME :props PROPS :data DATA :watch WATCH :compute COMPUTE)." (let ((format (tp--layer-group-element-format element))) (pcase format ('symbol element) ('format-4 - ;; Parse named layer with :props and optional :watch/:compute + ;; Parse named layer with :props and optional :data/:watch/:compute (let* ((layer-suffix (car element)) (layer-name (intern (format "%s-%s" group-name layer-suffix))) (rest (cdr element)) (props nil) + (data nil) (watch nil) (compute nil)) ;; Parse keyword arguments (while rest (pcase (car rest) (:props (setq props (cadr rest) rest (cddr rest))) + (:data (setq data (cadr rest) rest (cddr rest))) (:watch (setq watch (cadr rest) rest (cddr rest))) (:compute (setq compute (cadr rest) rest (cddr rest))) (_ (setq rest (cdr rest))))) - (list layer-name :props props :watch watch :compute compute))) + (list layer-name :props props :data data :watch watch :compute compute))) ('format-3 (let* ((layer-suffix (car element)) (layer-name (intern (format "%s-%s" group-name layer-suffix))) @@ -2068,20 +2125,21 @@ Format 3 - Named layers with :props keyword (named as NAME-suffix): (\"残月\" :props (display \"🌘\" face (:height 1.5))) (\"下弦月\" :props (display \"🌗\" face (:height 2.0)))) -Format 4 - Named layers with :props, :watch, and/or :compute (Vue 3 style): +Format 4 - Named layers with :props, :data, :watch, and/or :compute (Vue 3 style): (tp-define-layer-group group-name - (\"reactive\" :props (face (:foreground $my-color)) - :watch ((my-color (lambda (new old layer) (message \"Changed!\"))))) - (\"computed\" :props (face (:height 1.0)) - :compute (help-echo (lambda () (format \"Value: %s\" $my-var))))) + (\"reactive\" :props (face (:foreground $my-color) help-echo $full-name) + :data (first-name last-name) + :compute ((full-name (lambda () (concat first-name \" \" last-name)))) + :watch ((my-color (lambda (new old layer) (message \"Changed!\")))))) Reactive Variables: - If any symbol in the property specification starts with $, it is - treated as a reactive variable. When that variable's value changes, - all text regions with that layer will be automatically updated. + If any symbol in :props starts with $, it is treated as a reactive variable. + Variables in :data are also reactive. All reactive variables are automatically + defined as global variables if they are not already bound. +:data - A list of variable symbols for additional reactive state not in :props. +:compute - A list of (VAR-SYMBOL COMPUTE-FN) pairs for computed reactive variables. :watch - A list of (VAR-SYMBOL CALLBACK) pairs for side effects. -:compute - A plist of (PROP-KEY COMPUTE-FN) for derived properties. You can also reference already-defined layers by their symbol name: (tp-define-layer-group group-name @@ -2102,31 +2160,41 @@ and the group itself is stored in `tp-layer-groups'." ;; Reference to existing layer (symbol) ((symbolp parsed) (push parsed layer-names)) - ;; Extended format with :watch/:compute (format-4) - ;; parsed is (layer-name :props ... :watch ... :compute ...) + ;; Extended format with :data/:watch/:compute (format-4) + ;; parsed is (layer-name :props ... :data ... :watch ... :compute ...) ((and (listp parsed) (plist-get (cdr parsed) :props)) (let* ((layer-name (car parsed)) (props (plist-get (cdr parsed) :props)) + (data (plist-get (cdr parsed) :data)) (watch (plist-get (cdr parsed) :watch)) (compute (plist-get (cdr parsed) :compute)) (reactive-syms (tp--collect-reactive-symbols props)) - (compute-reactive-syms - (when compute - (cl-loop for (_key fn) on compute by #'cddr - append (tp--collect-reactive-symbols fn)))) - (all-reactive-syms (delete-dups (append reactive-syms compute-reactive-syms)))) - (if all-reactive-syms - ;; Has reactive symbols - register dependencies and resolve at runtime + (computed-vars (when compute (mapcar #'car compute))) + (all-reactive-syms (delete-dups reactive-syms)) + (all-vars-to-define (delete-dups + (append (mapcar #'tp--reactive-var-symbol reactive-syms) + data + computed-vars)))) + (if (or all-reactive-syms data compute) + ;; Has reactive features - register dependencies and resolve at runtime (push `(progn + ;; Ensure all reactive variables are defined + (tp--ensure-reactive-variables ',all-vars-to-define) + ;; Register data variables + ,@(when data + `((tp--register-layer-data ',layer-name ',data))) + ;; Register computed variable definitions + ,@(when compute + `((tp--register-layer-computed ',layer-name ',compute) + (tp--apply-initial-computed ',compute))) + ;; Register reactive dependencies (tp--register-reactive-deps ',layer-name ',all-reactive-syms ',props) + ;; Register watchers ,@(when watch `((tp--register-layer-watchers ',layer-name ',watch))) - ,@(when compute - `((tp--register-layer-computed ',layer-name ',compute))) + ;; Set layer properties with resolved values (let ((resolved-props (tp--resolve-reactive-symbols ',props))) - ,@(when compute - `((setq resolved-props (tp--apply-initial-computed resolved-props ',compute)))) (tp--set-layer-props ',layer-name resolved-props))) layer-defs) ;; No reactive symbols - use static properties @@ -2141,6 +2209,8 @@ and the group itself is stored in `tp-layer-groups'." (if reactive-syms ;; Has reactive symbols - register dependencies and resolve at runtime (push `(progn + (tp--ensure-reactive-variables + ',(mapcar #'tp--reactive-var-symbol reactive-syms)) (tp--register-reactive-deps ',layer-name ',reactive-syms ',props) (let ((resolved-props (tp--resolve-reactive-symbols ',props)))