refactor: redesign :compute to compute reactive variable values, add :data support
Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
parent
33f3ec3d7a
commit
36c0a60f3f
215
tp-tests.el
215
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
|
||||
|
||||
282
tp.el
282
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)))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user