From 7dad0f17893063496f0b777fe0fc80fe1da619f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 08:42:48 +0000 Subject: [PATCH] feat: add :watch and :compute support for tp-define-layer and tp-define-layer-group Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com> --- tp-tests.el | 202 ++++++++++++++++++++++++++++++++++++++ tp.el | 272 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 443 insertions(+), 31 deletions(-) diff --git a/tp-tests.el b/tp-tests.el index 83abc53..ae47ad1 100644 --- a/tp-tests.el +++ b/tp-tests.el @@ -2336,5 +2336,207 @@ Returns list of (START END VALUE) intervals." ;; Cleanup (makunbound 'tp-test-regexp-color)))) +;;; ============================================================ +;;; :watch and :compute Tests (Vue 3 style reactivity) +;;; ============================================================ + +(ert-deftest tp-test-define-layer-with-watch () + "Test tp-define-layer with :watch for side effects." + (tp-test-with-temp-buffer + (defvar tp-test-watch-var nil "Test variable for watch.") + (defvar tp-test-watch-log nil "Log of watch callback invocations.") + (setq tp-test-watch-var "initial") + (setq tp-test-watch-log nil) + (unwind-protect + (progn + (tp-define-layer test-watch-layer + :props (face (:foreground $tp-test-watch-var)) + :watch ((tp-test-watch-var + (lambda (new old layer) + (push (list new old layer) tp-test-watch-log))))) + ;; Check the layer is defined with resolved value + (let ((props (cdr (assoc 'test-watch-layer tp-layer-alist)))) + (should (equal (plist-get (plist-get props 'face) :foreground) "initial"))) + ;; Check the watcher is registered + (should (assoc 'test-watch-layer tp-layer-watchers)) + ;; Change the variable + (setq tp-test-watch-var "changed") + ;; Check the layer is updated + (let ((props (cdr (assoc 'test-watch-layer tp-layer-alist)))) + (should (equal (plist-get (plist-get props 'face) :foreground) "changed"))) + ;; Check the watcher was called + (should (= (length tp-test-watch-log) 1)) + (let ((log-entry (car tp-test-watch-log))) + (should (equal (nth 0 log-entry) "changed")) + (should (equal (nth 1 log-entry) "initial")) + (should (eq (nth 2 log-entry) 'test-watch-layer)))) + ;; Cleanup + (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." + (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-compute-layer + :props (face (:foreground $tp-test-compute-size)) + :compute (help-echo (lambda () (format "Size: %d" tp-test-compute-size)))) + ;; 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 + (let ((props (cdr (assoc 'test-compute-layer tp-layer-alist)))) + (should (equal (plist-get props 'help-echo) "Size: 10")))) + ;; Cleanup + (makunbound 'tp-test-compute-size)))) + +(ert-deftest tp-test-define-layer-with-watch-and-compute () + "Test tp-define-layer with both :watch and :compute." + (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")))) + ;; Cleanup + (makunbound 'tp-test-wc-color) + (makunbound 'tp-test-wc-log)))) + +(ert-deftest tp-test-define-layer-watch-requires-props () + "Test that :watch requires :props to be explicitly specified." + (tp-test-with-temp-buffer + (should-error + (macroexpand-1 + '(tp-define-layer test-invalid + :watch ((some-var (lambda (new old layer) nil)))))))) + +(ert-deftest tp-test-define-layer-compute-requires-props () + "Test that :compute requires :props to be explicitly specified." + (tp-test-with-temp-buffer + (should-error + (macroexpand-1 + '(tp-define-layer test-invalid + :compute (help-echo (lambda () "computed"))))))) + +(ert-deftest tp-test-undefine-layer-clears-watch-and-compute () + "Test tp-undefine-layer clears watchers and computed properties." + (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"))) + ;; Check registrations + (should (assoc 'test-undef-wc tp-layer-watchers)) + (should (assoc 'test-undef-wc tp-layer-computed)) + ;; 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))) + ;; Cleanup + (makunbound 'tp-test-undef-wc-var)))) + +(ert-deftest tp-test-define-layer-group-with-watch () + "Test tp-define-layer-group with :watch (format-4)." + (tp-test-with-temp-buffer + (defvar tp-test-group-watch-var nil "Test variable for group watch.") + (defvar tp-test-group-watch-log nil "Log of watch callback invocations.") + (setq tp-test-group-watch-var "red") + (setq tp-test-group-watch-log nil) + (unwind-protect + (progn + (tp-define-layer-group test-watch-group + ("reactive" :props (face (:foreground $tp-test-group-watch-var)) + :watch ((tp-test-group-watch-var + (lambda (new old layer) + (push (list new old layer) tp-test-group-watch-log))))) + ("static" :props (face (:foreground "blue")))) + ;; Check the group is defined + (should (assoc 'test-watch-group tp-layer-groups)) + ;; Check the reactive layer has its watcher registered + (should (assoc 'test-watch-group-reactive tp-layer-watchers)) + ;; Static layer should not have a watcher + (should-not (assoc 'test-watch-group-static tp-layer-watchers)) + ;; Change the variable + (setq tp-test-group-watch-var "green") + ;; Check the watcher was called + (should (= (length tp-test-group-watch-log) 1))) + ;; Cleanup + (makunbound 'tp-test-group-watch-var) + (makunbound 'tp-test-group-watch-log)))) + +(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 + (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)))) + ("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))) + ;; Cleanup + (makunbound 'tp-test-group-compute-var)))) + +(ert-deftest tp-test-reactive-reset-clears-watch-and-compute () + "Test tp-reactive-reset clears watchers and computed properties." + (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"))) + ;; Check registrations + (should tp-layer-watchers) + (should tp-layer-computed) + ;; Reset reactive + (tp-reactive-reset) + ;; Check all are cleared + (should-not tp-layer-watchers) + (should-not tp-layer-computed)) + ;; Cleanup + (makunbound 'tp-test-reset-wc-var)))) + (provide 'tp-ert-tests) ;;; tp-ert-tests.el ends here diff --git a/tp.el b/tp.el index 1adc8f4..bcb85e2 100644 --- a/tp.el +++ b/tp.el @@ -58,6 +58,18 @@ REACTIVE-PROPS contains only the property key-value pairs that use this variable For example, for (define-tp my-layer (help-echo \"test\" face (:foreground $color))), only (face (:foreground $color)) is stored, not the help-echo.") +(defvar tp-layer-watchers nil + "Alist mapping layer names to their watcher definitions. +Each element is (LAYER-NAME . WATCHER-LIST) where WATCHER-LIST +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.") + (defvar tp--anonymous-layer-counter 0 "Counter for generating unique anonymous layer names.") @@ -183,7 +195,10 @@ Only the reactive portions of the properties are stored for each variable." (remove-variable-watcher var-sym #'tp--reactive-variable-watcher))) ;; Clean up empty dependency entries (setq tp-reactive-deps - (cl-remove-if (lambda (dep) (null (cdr dep))) tp-reactive-deps))) + (cl-remove-if (lambda (dep) (null (cdr dep))) tp-reactive-deps)) + ;; Also clean up layer watchers and computed properties + (tp--unregister-layer-watchers layer-name) + (tp--unregister-layer-computed layer-name)) (defun tp--reactive-variable-watcher (symbol newval operation _where) "Watcher function called when a reactive variable changes. @@ -199,6 +214,7 @@ Only 'set' operations trigger updates because: (when (and (not (equal (symbol-value symbol) newval)) (eq operation 'set)) (let ((deps (cdr (assoc symbol tp-reactive-deps))) + (oldval (symbol-value symbol)) ;; Create override alist with the new value ;; (watcher is called before the variable is actually updated) (override-alist (list (cons symbol newval)))) @@ -207,6 +223,10 @@ Only 'set' operations trigger updates because: ;; Get the reactive props stored directly in the dependency (reactive-props (cdr dep))) (when reactive-props + ;; Call user-defined watch callbacks for this layer + (tp--invoke-layer-watchers layer-name symbol newval oldval) + ;; Update computed properties for this layer + (tp--update-layer-computed layer-name override-alist) ;; Resolve the reactive props with the new value override (let ((resolved-props (tp--resolve-reactive-symbols reactive-props override-alist))) @@ -220,6 +240,67 @@ Only 'set' operations trigger updates because: ;; Update all text regions with this layer (tp--update-layer-regions layer-name)))))))) +(defun tp--invoke-layer-watchers (layer-name symbol newval oldval) + "Invoke all registered watcher callbacks for LAYER-NAME watching SYMBOL. +NEWVAL is the new value, OLDVAL is the old value." + (when-let ((watchers (cdr (assoc layer-name tp-layer-watchers)))) + (dolist (watcher watchers) + (let ((watch-sym (car watcher)) + (callback (cdr watcher))) + (when (eq watch-sym symbol) + (condition-case err + (funcall callback newval oldval layer-name) + (error (message "tp: watcher error for %s watching %s: %s" + layer-name symbol 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." + (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))))) + +(defun tp--register-layer-watchers (layer-name watchers) + "Register WATCHERS for LAYER-NAME. +WATCHERS is a list of (VAR-SYMBOL CALLBACK) pairs." + (when watchers + (let ((watcher-pairs + (mapcar (lambda (watcher) + (cons (car watcher) (cadr watcher))) + watchers))) + (if (assoc layer-name tp-layer-watchers) + (setf (cdr (assoc layer-name tp-layer-watchers)) watcher-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." + (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)))) + +(defun tp--unregister-layer-watchers (layer-name) + "Unregister all watchers for LAYER-NAME." + (setq tp-layer-watchers (assq-delete-all layer-name tp-layer-watchers))) + +(defun tp--unregister-layer-computed (layer-name) + "Unregister all computed properties for LAYER-NAME." + (setq tp-layer-computed (assq-delete-all layer-name tp-layer-computed))) + (defun tp--update-layer-regions (layer-name) "Update all text regions that have LAYER-NAME applied. Re-applies the layer properties using tp-search-map and tp-add." @@ -242,12 +323,14 @@ Re-applies the layer properties using tp-search-map and tp-add." (defun tp-reactive-reset () "Reset all reactive text property watchers and dependencies." (interactive) - ;; Remove all watchers + ;; Remove all variable watchers (dolist (dep tp-reactive-deps) (let ((var-sym (car dep))) (remove-variable-watcher var-sym #'tp--reactive-variable-watcher))) - ;; Clear registries - (setq tp-reactive-deps nil)) + ;; Clear all registries + (setq tp-reactive-deps nil) + (setq tp-layer-watchers nil) + (setq tp-layer-computed nil)) ;;; Core Property Functions @@ -1770,52 +1853,105 @@ Returns a plist of all properties in the region or string." ;;; Layer Definition Functions +(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) + (cond + ;; Check for keyword arguments format + ((and (keywordp (car args)) + (memq (car args) '(:props :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))) + (: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"))) + ;; 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))) + (defmacro tp-define-layer (name &rest args) "Define a single text property layer named NAME. -This macro supports two formats: +This macro supports three formats: -Format 1 - Direct plist: +Format 1 - Direct plist (legacy, no :watch/:compute support): (tp-define-layer layer-name (display \"🌑\" face (:height 1.0))) -Format 2 - With :props keyword (for future extensibility): +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): + (tp-define-layer layer-name + :props (face (:foreground $my-color) help-echo \"tip\") + :watch ((my-color (lambda (new old layer) + (message \"Color changed from %s to %s\" old new)))) + :compute (derived-prop (lambda () (concat \"Value: \" $my-var)))) + 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. - Example: - (defvar my-color \"red\") - (tp-define-layer my-layer - (face (:foreground $my-color))) - ;; Changing my-color will automatically update text with my-layer +: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. + +: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. + +Note: When using :watch or :compute, 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. The layer is stored in `tp-layer-alist'." (declare (indent defun)) - (let* ((properties - (cond - ;; Format 2: :props (plist) - ((and (eq (car args) :props) - (cadr args)) - (cadr args)) - ;; Format 1: (plist) - ((and (= (length args) 1) - (listp (car args))) - (car args)) - (t (error "Invalid tp-define-layer format for %s" name)))) - (reactive-syms (tp--collect-reactive-symbols properties))) - (if reactive-syms + (let* ((parsed (tp--parse-define-layer-args args)) + (properties (plist-get parsed :props)) + (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 `(progn - (tp--register-reactive-deps ',name ',reactive-syms ',properties) + (tp--register-reactive-deps ',name ',all-reactive-syms ',properties) + ,@(when watch + `((tp--register-layer-watchers ',name ',watch))) + ,@(when compute + `((tp--register-layer-computed ',name ',compute))) (let ((resolved-props (tp--resolve-reactive-symbols ',properties))) + ;; Apply initial computed values + ,@(when compute + `((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))))))) (tp--set-layer-props ',name resolved-props)) (assoc ',name tp-layer-alist)) ;; No reactive symbols - use static properties @@ -1827,10 +1963,21 @@ The layer is stored in `tp-layer-alist'." (defun tp--layer-group-element-format (element) "Determine the format type of ELEMENT. -Returns 'symbol, 'format-1, 'format-2, 'format-3, or nil if invalid." +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 + ((and (listp element) + (> (length element) 3) + (stringp (car element)) + (eq (cadr element) :props) + (listp (caddr element)) + ;; Must have additional keywords after :props + (let ((rest (cdddr element))) + (and rest (keywordp (car rest))))) + 'format-4) ;; Format 3 - ("name" :props (plist...)) ((and (listp element) (= (length element) 3) @@ -1851,16 +1998,33 @@ Returns 'symbol, 'format-1, 'format-2, 'format-3, or nil if invalid." (t nil))) (defun tp--parse-layer-group-element (group-name element idx) - "Parse a layer group element and return (layer-name . properties). + "Parse a layer group element and return (layer-name . properties) or extended form. GROUP-NAME is the name of the layer group. ELEMENT is the element to parse (can be anonymous plist, cons-cell, or :props form). IDX is the index for anonymous elements. Returns a cons cell (LAYER-NAME . PROPERTIES) or a symbol if ELEMENT -references an already-defined layer." +references an already-defined layer. +For format-4 elements, returns (LAYER-NAME :props PROPS :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 + (let* ((layer-suffix (car element)) + (layer-name (intern (format "%s-%s" group-name layer-suffix))) + (rest (cdr element)) + (props nil) + (watch nil) + (compute nil)) + ;; Parse keyword arguments + (while rest + (pcase (car rest) + (:props (setq props (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))) ('format-3 (let* ((layer-suffix (car element)) (layer-name (intern (format "%s-%s" group-name layer-suffix))) @@ -1879,7 +2043,7 @@ references an already-defined layer." (defmacro tp-define-layer-group (name &rest elements) "Define a layer group named NAME containing multiple layers. -This macro supports three formats for each element: +This macro supports four formats for each element: Format 1 - Anonymous layers (named as NAME-0, NAME-1, etc.): (tp-define-layer-group group-name @@ -1899,11 +2063,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): + (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 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. +: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 existing-layer-1 @@ -1923,7 +2097,43 @@ and the group itself is stored in `tp-layer-groups'." ;; Reference to existing layer (symbol) ((symbolp parsed) (push parsed layer-names)) - ;; New layer definition (cons cell of name . props) + ;; Extended format with :watch/:compute (format-4) + ;; parsed is (layer-name :props ... :watch ... :compute ...) + ((and (listp parsed) (plist-get (cdr parsed) :props)) + (let* ((layer-name (car parsed)) + (props (plist-get (cdr parsed) :props)) + (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 + (push `(progn + (tp--register-reactive-deps + ',layer-name ',all-reactive-syms ',props) + ,@(when watch + `((tp--register-layer-watchers ',layer-name ',watch))) + ,@(when compute + `((tp--register-layer-computed ',layer-name ',compute))) + (let ((resolved-props (tp--resolve-reactive-symbols ',props))) + ,@(when compute + `((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))))))) + (tp--set-layer-props ',layer-name resolved-props))) + layer-defs) + ;; No reactive symbols - use static properties + (push `(tp--set-layer-props ',layer-name ',props) + layer-defs)) + (push layer-name layer-names))) + ;; Simple format (cons cell of name . props) ((consp parsed) (let* ((layer-name (car parsed)) (props (cdr parsed))