fix: :data variables now trigger computed value updates via setq-local

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-12-20 11:00:22 +00:00
parent c3ddeede64
commit ac0e163e66
2 changed files with 97 additions and 37 deletions

View File

@ -27,6 +27,7 @@
`(with-temp-buffer `(with-temp-buffer
(setq tp-layer-alist nil) (setq tp-layer-alist nil)
(setq tp-layer-groups nil) (setq tp-layer-groups nil)
(tp-reactive-reset)
,@body)) ,@body))
;;; ============================================================ ;;; ============================================================
@ -2426,17 +2427,11 @@ Returns list of (START END VALUE) intervals."
(tp-test-with-temp-buffer (tp-test-with-temp-buffer
(unwind-protect (unwind-protect
(progn (progn
(tp-define-layer test-dc-layer ;; Set data values first
: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-color "blue")
(setq tp-test-dc-first "Jane") (setq tp-test-dc-first "Jane")
(setq tp-test-dc-last "Smith") (setq tp-test-dc-last "Smith")
;; Re-eval to trigger the compute ;; Define layer with :data and :compute
(tp-define-layer test-dc-layer (tp-define-layer test-dc-layer
:props (face (:foreground $tp-test-dc-color) help-echo $tp-test-dc-full-name) :props (face (:foreground $tp-test-dc-color) help-echo $tp-test-dc-full-name)
:data (tp-test-dc-first tp-test-dc-last) :data (tp-test-dc-first tp-test-dc-last)
@ -2450,10 +2445,10 @@ Returns list of (START END VALUE) intervals."
;; Check the computed value ;; Check the computed value
(should (equal tp-test-dc-full-name "Jane Smith"))) (should (equal tp-test-dc-full-name "Jane Smith")))
;; Cleanup ;; Cleanup
(makunbound 'tp-test-dc-color) (ignore-errors (makunbound 'tp-test-dc-color))
(makunbound 'tp-test-dc-first) (ignore-errors (makunbound 'tp-test-dc-first))
(makunbound 'tp-test-dc-last) (ignore-errors (makunbound 'tp-test-dc-last))
(makunbound 'tp-test-dc-full-name)))) (ignore-errors (makunbound 'tp-test-dc-full-name)))))
(ert-deftest tp-test-define-layer-watch-requires-props () (ert-deftest tp-test-define-layer-watch-requires-props ()
"Test that :watch requires :props to be explicitly specified." "Test that :watch requires :props to be explicitly specified."
@ -2629,5 +2624,35 @@ Returns list of (START END VALUE) intervals."
;; Cleanup ;; Cleanup
(makunbound 'tp-test-local-color)))) (makunbound 'tp-test-local-color))))
(ert-deftest tp-test-data-setq-local-triggers-compute ()
"Test that setq-local on :data variables triggers computed value updates."
(tp-test-with-temp-buffer
(unwind-protect
(progn
;; Define layer with :data and :compute
(tp-define-layer test-data-compute-layer
:props (help-echo $tp-test-dc-full)
:data (tp-test-dc-first tp-test-dc-last)
:compute ((tp-test-dc-full
(lambda ()
(concat tp-test-dc-first " " tp-test-dc-last)))))
;; Apply layer to text
(insert "Hello World")
(tp-set 1 6 'test-data-compute-layer)
;; Initial computed value should be " " (concat nil nil = " ")
(should (equal (get-text-property 1 'help-echo) " "))
;; Use setq-local to set first name
(setq-local tp-test-dc-first "Kinney")
;; Computed should be "Kinney " now
(should (equal (get-text-property 1 'help-echo) "Kinney "))
;; Use setq-local to set last name
(setq-local tp-test-dc-last "Zhang")
;; Computed should be "Kinney Zhang" now
(should (equal (get-text-property 1 'help-echo) "Kinney Zhang")))
;; Cleanup
(ignore-errors (makunbound 'tp-test-dc-first))
(ignore-errors (makunbound 'tp-test-dc-last))
(ignore-errors (makunbound 'tp-test-dc-full)))))
(provide 'tp-ert-tests) (provide 'tp-ert-tests)
;;; tp-ert-tests.el ends here ;;; tp-ert-tests.el ends here

61
tp.el
View File

@ -161,15 +161,15 @@ override the current variable values (used during watcher callbacks)."
(tp--resolve-reactive-symbols (cdr form) override-alist))) (tp--resolve-reactive-symbols (cdr form) override-alist)))
(t form))) (t form)))
(defun tp--register-reactive-deps (layer-name reactive-symbols template-props) (defun tp--register-reactive-deps (layer-name reactive-symbols props)
"Register REACTIVE-SYMBOLS as dependencies for LAYER-NAME. "Register REACTIVE-SYMBOLS as dependencies for LAYER-NAME.
TEMPLATE-PROPS is the original property specification with reactive symbols. PROPS is the original property specification with reactive symbols.
Only the reactive portions of the properties are stored for each variable." Only the reactive portions of the properties are stored for each variable."
;; Register each reactive symbol's dependency with only its relevant properties ;; Register each reactive symbol's dependency with only its relevant properties
(dolist (rsym reactive-symbols) (dolist (rsym reactive-symbols)
(let* ((var-sym (tp--reactive-var-symbol rsym)) (let* ((var-sym (tp--reactive-var-symbol rsym))
;; Extract only the properties that use this specific reactive variable ;; Extract only the properties that use this specific reactive variable
(reactive-props (tp--extract-reactive-props template-props rsym)) (reactive-props (tp--extract-reactive-props props rsym))
(existing (assoc var-sym tp-reactive-deps))) (existing (assoc var-sym tp-reactive-deps)))
(if existing (if existing
;; Update or add this layer to existing dependencies ;; Update or add this layer to existing dependencies
@ -228,23 +228,23 @@ Only 'set' operations trigger updates because:
(let* ((layer-name (car dep)) (let* ((layer-name (car dep))
;; Get the reactive props stored directly in the dependency ;; Get the reactive props stored directly in the dependency
(reactive-props (cdr dep))) (reactive-props (cdr dep)))
(when reactive-props
;; Call user-defined watch callbacks for this layer ;; Call user-defined watch callbacks for this layer
(tp--invoke-layer-watchers layer-name symbol newval oldval) (tp--invoke-layer-watchers layer-name symbol newval oldval)
;; Update computed properties for this layer ;; Update computed properties for this layer
(tp--update-layer-computed layer-name override-alist) (let ((updated-override (tp--update-layer-computed layer-name override-alist)))
(when reactive-props
;; Resolve the reactive props with the new value override ;; Resolve the reactive props with the new value override
(let ((resolved-props (tp--resolve-reactive-symbols (let ((resolved-props (tp--resolve-reactive-symbols
reactive-props override-alist))) reactive-props updated-override)))
;; Update only the reactive properties in the layer definition ;; Update only the reactive properties in the layer definition
(let ((current-props (cdr (assoc layer-name tp-layer-alist)))) (let ((current-props (cdr (assoc layer-name tp-layer-alist))))
(when current-props (when current-props
;; Merge the resolved reactive props into the current layer props ;; Merge the resolved reactive props into the current layer props
(cl-loop for (key val) on resolved-props by #'cddr (cl-loop for (key val) on resolved-props by #'cddr
do (setq current-props (plist-put current-props key val))) do (setq current-props (plist-put current-props key val)))
(tp--set-layer-props layer-name current-props))) (tp--set-layer-props layer-name current-props))))))
;; Update all text regions with this layer ;; Update all text regions with this layer
(tp--update-layer-regions layer-name)))))))) (tp--update-layer-regions layer-name))))))
(defun tp--invoke-layer-watchers (layer-name symbol newval oldval) (defun tp--invoke-layer-watchers (layer-name symbol newval oldval)
"Invoke all registered watcher callbacks for LAYER-NAME watching SYMBOL. "Invoke all registered watcher callbacks for LAYER-NAME watching SYMBOL.
@ -267,9 +267,14 @@ Returns an updated override-alist with the new computed values."
(dolist (comp computed) (dolist (comp computed)
(let* ((var-sym (car comp)) (let* ((var-sym (car comp))
(compute-fn (cdr comp)) (compute-fn (cdr comp))
;; Temporarily bind variables to their new values from override-alist
;; before calling the compute function
(computed-val (computed-val
(condition-case err (condition-case err
(funcall compute-fn) (cl-progv
(mapcar #'car override-alist)
(mapcar #'cdr override-alist)
(funcall compute-fn))
(error (error
(message "tp: compute error for %s.%s: %s" (message "tp: compute error for %s.%s: %s"
layer-name var-sym err) layer-name var-sym err)
@ -278,7 +283,25 @@ Returns an updated override-alist with the new computed values."
;; Update the global variable ;; Update the global variable
(set var-sym computed-val) (set var-sym computed-val)
;; Add to override-alist for property resolution ;; Add to override-alist for property resolution
(push (cons var-sym computed-val) override-alist))))) (push (cons var-sym computed-val) override-alist)
;; Also update the layer properties if the computed var is used in props
(let ((current-props (cdr (assoc layer-name tp-layer-alist))))
(when current-props
;; Collect all reactive props for this layer from tp-reactive-deps
(let ((all-reactive-props nil))
(dolist (dep tp-reactive-deps)
(let ((layer-entry (assoc layer-name (cdr dep))))
(when (and layer-entry (cdr layer-entry))
;; Merge the reactive props
(cl-loop for (key val) on (cdr layer-entry) by #'cddr
do (setq all-reactive-props
(plist-put all-reactive-props key val))))))
(when all-reactive-props
(let ((resolved-props (tp--resolve-reactive-symbols all-reactive-props override-alist)))
(when resolved-props
(cl-loop for (key val) on resolved-props by #'cddr
do (setq current-props (plist-put current-props key val)))
(tp--set-layer-props layer-name current-props)))))))))))
override-alist) override-alist)
(defun tp--register-layer-watchers (layer-name watchers) (defun tp--register-layer-watchers (layer-name watchers)
@ -330,11 +353,23 @@ Sets the global variables to their computed values."
(defun tp--register-layer-data (layer-name data-vars) (defun tp--register-layer-data (layer-name data-vars)
"Register DATA-VARS for LAYER-NAME. "Register DATA-VARS for LAYER-NAME.
DATA-VARS is a list of variable symbols defined via :data." DATA-VARS is a list of variable symbols defined via :data.
Also adds variable watchers so changes to data vars trigger computed updates."
(when data-vars (when data-vars
(if (assoc layer-name tp-layer-data) (if (assoc layer-name tp-layer-data)
(setf (cdr (assoc layer-name tp-layer-data)) data-vars) (setf (cdr (assoc layer-name tp-layer-data)) data-vars)
(push (cons layer-name data-vars) tp-layer-data)))) (push (cons layer-name data-vars) tp-layer-data))
;; Add watchers for data variables
(dolist (var-sym data-vars)
(let ((existing (assoc var-sym tp-reactive-deps)))
(if existing
;; Add this layer to existing dependencies (with nil props since data vars don't have direct props)
(let ((layer-entry (assoc layer-name (cdr existing))))
(unless layer-entry
(push (cons layer-name nil) (cdr existing))))
;; Create new dependency entry and add watcher
(push (cons var-sym (list (cons layer-name nil))) tp-reactive-deps)
(add-variable-watcher var-sym #'tp--reactive-variable-watcher))))))
(defun tp--unregister-layer-data (layer-name) (defun tp--unregister-layer-data (layer-name)
"Unregister data variables for LAYER-NAME." "Unregister data variables for LAYER-NAME."
@ -2293,7 +2328,7 @@ For group names, includes `tp-layers' property with the full layer stack."
(resolved-props (tp--resolve-reactive-symbols props))) (resolved-props (tp--resolve-reactive-symbols props)))
;; Register this anonymous layer in tp-layer-alist with resolved props ;; Register this anonymous layer in tp-layer-alist with resolved props
(tp--set-layer-props layer-name resolved-props) (tp--set-layer-props layer-name resolved-props)
;; Register reactive dependencies with the original template props ;; Register reactive dependencies with the original props
(tp--register-reactive-deps layer-name reactive-syms props) (tp--register-reactive-deps layer-name reactive-syms props)
;; Return resolved props with tp-name ;; Return resolved props with tp-name
(append resolved-props (list 'tp-name layer-name))) (append resolved-props (list 'tp-name layer-name)))