diff --git a/tp-tests.el b/tp-tests.el index b309f2f..39990b0 100644 --- a/tp-tests.el +++ b/tp-tests.el @@ -3622,5 +3622,85 @@ When using tp-set (direct property setting), tp-name is NOT added." ;; Valid parameterized layer should work (should (define-tp tp-test-param-valid (value) `(face (:height ,value)))))) +;;; ============================================================ +;;; Nested Layer Resolution Tests +;;; ============================================================ + +(ert-deftest tp-test-nested-layer-resolution () + "Test that nested custom layers are resolved to built-in properties. +When a layer's body returns a plist containing other custom layer names, +those should be recursively expanded to their built-in properties." + (tp-test-with-temp-buffer + ;; Define a base layer that returns built-in properties + (define-tp tp-test-base-layer (color) + `(face (:foreground ,color :background "white"))) + ;; Define a wrapper layer that uses the base layer + (define-tp tp-test-wrapper-layer (plist) + (let ((color (plist-get plist :color))) + `(tp-test-base-layer ,color + help-echo "wrapper"))) + ;; Use the wrapper layer + (let ((result (tp-set "test" 'tp-test-wrapper-layer '(:color "red")))) + ;; The face property should be resolved from tp-test-base-layer + (should (equal (plist-get (get-text-property 0 'face result) :foreground) "red")) + (should (equal (plist-get (get-text-property 0 'face result) :background) "white")) + ;; help-echo should also be present + (should (equal (get-text-property 0 'help-echo result) "wrapper")) + ;; tp-test-base-layer should NOT be present as a property + (should (null (get-text-property 0 'tp-test-base-layer result)))))) + +(ert-deftest tp-test-nested-layer-resolution-with-tp-palette () + "Test nested layer resolution with tp-palette and tp-button pattern. +This tests the exact use case from the issue: tp-button uses tp-palette +internally, and the final result should have face properties, not tp-palette." + (tp-test-with-temp-buffer + ;; Re-define tp-palette and tp-button since tp-test-with-temp-buffer resets tp-layer-alist + (define-tp tp-palette (symbol) + (let ((palette (intern (concat "tp-palette-" + (symbol-name symbol))))) + `(face ( :foreground ,(tp-palette-fg-color palette) + :background ,(tp-palette-bg-color palette) + :box (:color ,(tp-palette-border-color palette)))))) + (define-tp tp-button (plist) + (let ((palette (plist-get plist :palette)) + (action (plist-get plist :action))) + `( tp-palette ,palette + keymap ,(let ((keymap (make-sparse-keymap))) + (define-key keymap (kbd "") action) + (define-key keymap [mouse-1] action) + keymap)))) + ;; Test that using tp-button resolves tp-palette to face properties + (let ((result (tp-set "emacs" 'tp-button '(:palette org-code)))) + ;; The face property should be resolved from tp-palette + (let ((face-prop (get-text-property 0 'face result))) + (should face-prop) + (should (plist-get face-prop :foreground)) + (should (plist-get face-prop :background)) + (should (plist-get face-prop :box))) + ;; keymap should also be present + (should (get-text-property 0 'keymap result)) + ;; tp-palette should NOT be present as a property + (should (null (get-text-property 0 'tp-palette result)))))) + +(ert-deftest tp-test-deeply-nested-layer-resolution () + "Test that deeply nested layers (3 levels) are fully resolved." + (tp-test-with-temp-buffer + ;; Define 3 levels of nesting + (define-tp tp-test-level1 (val) + `(face (:foreground ,val))) + (define-tp tp-test-level2 (val) + `(tp-test-level1 ,val help-echo "level2")) + (define-tp tp-test-level3 (val) + `(tp-test-level2 ,val display "level3")) + ;; Use the most deeply nested layer + (let ((result (tp-set "test" 'tp-test-level3 "blue"))) + ;; All properties should be resolved + (should (equal (plist-get (get-text-property 0 'face result) :foreground) "blue")) + (should (equal (get-text-property 0 'help-echo result) "level2")) + (should (equal (get-text-property 0 'display result) "level3")) + ;; None of the custom layer names should be present + (should (null (get-text-property 0 'tp-test-level1 result))) + (should (null (get-text-property 0 'tp-test-level2 result)))))) + (provide 'tp-ert-tests) ;;; tp-ert-tests.el ends here diff --git a/tp.el b/tp.el index f481043..6afc5c9 100644 --- a/tp.el +++ b/tp.el @@ -2634,7 +2634,8 @@ Also includes tp-name automatically if the layer has reactive dependencies regis Handles two storage formats: 1. Old format (from tp--set-layer-props): (LAYER-NAME . PLIST) - flat plist 2. Unified format (from define-tp): (LAYER-NAME ARGLIST BODY-FORM) -For parameterized layers (ARGLIST non-nil), returns nil - use `tp-layer-props-with-arg'." +For parameterized layers (ARGLIST non-nil), returns nil - use `tp-layer-props-with-arg'. +Recursively expands any nested layer names in the returned plist." (when-let ((entry (cdr (assoc layer-name tp-layer-alist)))) ;; Auto-include tp-name for layers with reactive deps (let ((needs-tp-name (or include-tp-name @@ -2654,14 +2655,21 @@ For parameterized layers (ARGLIST non-nil), returns nil - use `tp-layer-props-wi ;; Non-parameterized - evaluate body and return props (let ((plist (eval body))) (when plist + ;; Recursively expand nested layer names + (when (tp--plist-has-layer-key-p plist) + (setq plist (tp--expand-layer-in-plist plist))) (if needs-tp-name (append plist (list 'tp-name layer-name)) plist)))))) ;; Old format: entry is just a flat plist (t - (if needs-tp-name - (append entry (list 'tp-name layer-name)) - entry)))))) + (let ((plist entry)) + ;; Recursively expand nested layer names + (when (tp--plist-has-layer-key-p plist) + (setq plist (tp--expand-layer-in-plist plist))) + (if needs-tp-name + (append plist (list 'tp-name layer-name)) + plist))))))) (defun tp-layer-parameterized-p (layer-name) "Return non-nil if LAYER-NAME is a parameterized layer. @@ -2678,7 +2686,8 @@ where ARGLIST is a non-nil list of argument symbols." (defun tp-layer-props-with-arg (layer-name arg &optional include-tp-name) "Return properties for parameterized layer LAYER-NAME with ARG. Evaluates the body form with the argument bound to the parameter. -If INCLUDE-TP-NAME is non-nil, appends 'tp-name property to identify the layer." +If INCLUDE-TP-NAME is non-nil, appends 'tp-name property to identify the layer. +Recursively expands any nested layer names in the returned plist." (when-let ((entry (cdr (assoc layer-name tp-layer-alist)))) ;; entry is (ARGLIST BODY-FORM) (let ((arglist (car entry)) @@ -2688,6 +2697,9 @@ If INCLUDE-TP-NAME is non-nil, appends 'tp-name property to identify the layer." ;; Evaluate the body with the argument bound (plist (eval `(let ((,arg-sym ',arg)) ,body)))) (when plist + ;; Recursively expand nested layer names + (when (tp--plist-has-layer-key-p plist) + (setq plist (tp--expand-layer-in-plist plist))) (if include-tp-name (append plist (list 'tp-name layer-name)) plist))))))) @@ -2706,10 +2718,16 @@ If INCLUDE-TP-NAME is non-nil, each layer's props will include tp-name." (or (assoc sym tp-layer-alist) (assoc sym tp-layer-groups)))) +(defun tp--plist-has-layer-key-p (plist) + "Return non-nil if PLIST contains any layer names as keys." + (cl-loop for (key _val) on plist by #'cddr + thereis (tp--is-layer-name-p key))) + (defun tp--expand-layer-in-plist (props) "Expand any layer names found in PROPS plist. Scans through PROPS treating it as a plist (key value pairs). When a key is a layer/group name, expands it with its properties. +Recursively expands until no more layer names are found in the result. Does NOT add tp-name - this is for direct property setting (tp-set/add/reset). Returns the expanded plist." (let ((result nil) @@ -2736,6 +2754,9 @@ Returns the expanded plist." ;; Reverse so first layer's properties are applied last (take precedence) (apply #'append (reverse layer-props-list))))))) (when layer-props + ;; Recursively expand if the layer props contain more layer names + (when (tp--plist-has-layer-key-p layer-props) + (setq layer-props (tp--expand-layer-in-plist layer-props))) (setq result (append result layer-props))))) ;; Regular property - keep as-is (t