feat: Support anonymous reactive layers with auto-generated tp-name

- Add tp--generate-anonymous-layer-name to create unique layer names (tp-anon-*)
- Update tp--resolve-props to handle anonymous plists:
  - If plist has reactive variables ($...), generate tp-name and register dependencies
  - All anonymous plists now get a tp-name for reactive support
- Update tp--parse-args to process all props through tp--resolve-props
- Update tp--ensure-props to handle plists with reactive variables
- Add tests for anonymous reactive layers with tp-set, tp-match-set, tp-regexp-set

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-12-20 07:51:27 +00:00
parent 736cfc59d9
commit 56733e7473
2 changed files with 147 additions and 13 deletions

View File

@ -2242,5 +2242,99 @@ Returns list of (START END VALUE) intervals."
(should (equal (plist-get (plist-get face :underline) :style) 'wave)))
(should (equal (tp-at 1 'help-echo) "complex"))))
;;; ============================================================
;;; Anonymous Layer and Reactive Text Property Tests
;;; ============================================================
(ert-deftest tp-test-set-anonymous-layer-gets-tp-name ()
"Test that tp-set with anonymous plist gets a tp-name."
(tp-test-with-temp-buffer
(insert "Hello World")
(tp-set 1 6 '(face bold))
;; Anonymous layer should have a generated tp-name
(should (tp-at 1 'tp-name))
;; The tp-name should be a symbol starting with tp-anon-
(should (string-prefix-p "tp-anon-" (symbol-name (tp-at 1 'tp-name))))))
(ert-deftest tp-test-set-anonymous-reactive-layer ()
"Test that tp-set with anonymous reactive plist works."
(tp-test-with-temp-buffer
(defvar tp-test-anon-color nil "Test variable for anonymous reactive layer.")
(setq tp-test-anon-color "red")
(unwind-protect
(progn
(insert "Hello World")
;; Set with anonymous reactive plist
(tp-set 1 6 '(face (:foreground $tp-test-anon-color)))
;; Should have resolved the reactive variable
(let ((face (tp-at 1 'face)))
(should (equal (plist-get face :foreground) "red")))
;; Should have a generated tp-name
(should (tp-at 1 'tp-name))
;; The reactive variable should be registered in dependencies
(should (assoc 'tp-test-anon-color tp-reactive-deps))
;; Change the variable - should update the text
(setq tp-test-anon-color "blue")
(let ((face (tp-at 1 'face)))
(should (equal (plist-get face :foreground) "blue"))))
;; Cleanup
(makunbound 'tp-test-anon-color))))
(ert-deftest tp-test-set-anonymous-layer-preserves-existing-tp-name ()
"Test that tp-set with anonymous plist preserves existing tp-name."
(tp-test-with-temp-buffer
(insert "Hello World")
;; First set with a layer name
(tp-define-layer my-existing-layer (face bold))
(tp-set 1 6 'my-existing-layer)
(should (eq (tp-at 1 'tp-name) 'my-existing-layer))
;; Now set with anonymous plist that already has tp-name
(tp-set 1 6 '(face italic tp-name my-custom-name))
(should (eq (tp-at 1 'tp-name) 'my-custom-name))))
(ert-deftest tp-test-match-set-anonymous-reactive-layer ()
"Test that tp-match-set with anonymous reactive plist works."
(tp-test-with-temp-buffer
(defvar tp-test-match-color nil "Test variable for match reactive layer.")
(setq tp-test-match-color "green")
(unwind-protect
(progn
(insert "Hello World Hello")
;; Set with anonymous reactive plist
(tp-match-set "Hello" '(face (:foreground $tp-test-match-color)))
;; Should have resolved the reactive variable
(let ((face (tp-at 1 'face)))
(should (equal (plist-get face :foreground) "green")))
;; Should have a generated tp-name
(should (tp-at 1 'tp-name))
;; Change the variable - should update the text
(setq tp-test-match-color "yellow")
(let ((face (tp-at 1 'face)))
(should (equal (plist-get face :foreground) "yellow"))))
;; Cleanup
(makunbound 'tp-test-match-color))))
(ert-deftest tp-test-regexp-set-anonymous-reactive-layer ()
"Test that tp-regexp-set with anonymous reactive plist works."
(tp-test-with-temp-buffer
(defvar tp-test-regexp-color nil "Test variable for regexp reactive layer.")
(setq tp-test-regexp-color "purple")
(unwind-protect
(progn
(insert "abc 123 def 456")
;; Set with anonymous reactive plist
(tp-regexp-set "[0-9]+" '(face (:foreground $tp-test-regexp-color)))
;; Should have resolved the reactive variable
(let ((face (tp-at 5 'face)))
(should (equal (plist-get face :foreground) "purple")))
;; Should have a generated tp-name
(should (tp-at 5 'tp-name))
;; Change the variable - should update the text
(setq tp-test-regexp-color "orange")
(let ((face (tp-at 5 'face)))
(should (equal (plist-get face :foreground) "orange"))))
;; Cleanup
(makunbound 'tp-test-regexp-color))))
(provide 'tp-ert-tests)
;;; tp-ert-tests.el ends here

66
tp.el
View File

@ -61,6 +61,17 @@ only (face (:foreground $color)) is stored, not the help-echo.")
(defvar tp-reactive-enabled t
"Non-nil means reactive text property updates are enabled.")
(defvar tp--anonymous-layer-counter 0
"Counter for generating unique anonymous layer names.")
(defun tp--generate-anonymous-layer-name ()
"Generate a unique symbol for anonymous layers.
Uses a combination of timestamp and counter to ensure uniqueness."
(setq tp--anonymous-layer-counter (1+ tp--anonymous-layer-counter))
(intern (format "tp-anon-%s-%d"
(format-time-string "%s%N")
tp--anonymous-layer-counter)))
;;; Reactive Text Properties Functions
@ -250,8 +261,12 @@ Supports four calling conventions:
3. String region: (START END PROPS STRING)
4. Entire string: (STRING PROP VAL ...)
PROPS can also be a symbol representing a layer or group name defined
by `define-tp' or `define-tp-group', which will be resolved to its properties."
PROPS can be:
- A symbol representing a layer or group name defined by `define-tp' or `define-tp-group'
- A plist of properties (anonymous layers get a generated tp-name)
For anonymous plists with reactive variables ($...), a unique tp-name is generated
and reactive dependencies are registered automatically."
(let (object start finish props)
(cond
;; First arg is a string - apply to entire string
@ -275,10 +290,6 @@ by `define-tp' or `define-tp-group', which will be resolved to its properties."
(setq object nil
props props-or-val)))
(t (error "Invalid first argument: %S" start-or-string)))
;; Resolve layer/group name to properties if props is a symbol.
;; This allows passing layer names like 'my-layer instead of property lists.
(when (symbolp props)
(setq props (or (tp--resolve-props props) props)))
;; Unwrap double-wrapped properties: when called as (tp-set 1 6 '(face bold)),
;; props is already the plist. But when called internally or from certain
;; contexts, props might be wrapped in an extra list like '((face bold)).
@ -286,6 +297,11 @@ by `define-tp' or `define-tp-group', which will be resolved to its properties."
;; a list (not just a symbol like 'face).
(when (and (listp props) (listp (car-safe props)))
(setq props (car props)))
;; Resolve props: handles layer/group names and anonymous reactive plists.
;; For symbols: resolves to layer/group properties with tp-name/tp-layers.
;; For plists: adds tp-name and handles reactive variables.
(when props
(setq props (or (tp--resolve-props props) props)))
(list object start finish props)))
(defun tp-set (start-or-string &optional end-or-prop props-or-val &rest rest)
@ -1956,19 +1972,43 @@ Appends 'tp-name property to identify the layer."
"Resolve PROPS to a property list with layer metadata.
PROPS can be:
- A symbol (layer name from `tp-layer-alist' or group name from `tp-layer-groups')
- A plist (returned as-is)
- A plist (handles anonymous layers with reactive variables)
If PROPS is a symbol:
- First checks `tp-layer-alist' and returns the layer properties WITH `tp-name'
- Then checks `tp-layer-groups' and returns properties WITH `tp-layers'
If PROPS is a plist:
- If it contains reactive variables ($...), generates a UUID for `tp-name',
registers reactive dependencies, and returns the resolved props with `tp-name'.
If the plist already has a `tp-name', uses that instead of generating a new one.
- If no reactive variables, adds a `tp-name' for the anonymous layer.
Returns nil if PROPS is a symbol but no matching layer/group is found.
For layer names, includes `tp-name' property for reactive text property support.
For group names, includes `tp-layers' property with the full layer stack."
(cond
;; Already a plist - return as-is
((listp props) props)
;; Already a plist - check for reactive variables and add tp-name
((listp props)
(let* ((existing-tp-name (plist-get props 'tp-name))
(reactive-syms (tp--collect-reactive-symbols props)))
(if reactive-syms
;; Has reactive symbols - need to handle as anonymous reactive layer
(let* ((layer-name (or existing-tp-name (tp--generate-anonymous-layer-name)))
;; Resolve reactive symbols to get current values
(resolved-props (tp--resolve-reactive-symbols props)))
;; Register this anonymous layer in tp-layer-alist with original template
(tp--set-layer-props layer-name (tp--resolve-reactive-symbols props))
;; Register reactive dependencies with the original template props
(tp--register-reactive-deps layer-name reactive-syms props)
;; Return resolved props with tp-name
(append resolved-props (list 'tp-name layer-name)))
;; No reactive symbols - just add tp-name if not present
(if existing-tp-name
props
(let ((layer-name (tp--generate-anonymous-layer-name)))
(append props (list 'tp-name layer-name)))))))
;; Symbol - check if it's a layer or group name
((symbolp props)
(cond
@ -1985,12 +2025,12 @@ For group names, includes `tp-layers' property with the full layer stack."
(t nil)))
(defun tp--ensure-props (plist)
"Ensure PLIST is a property list, resolving layer names if needed.
"Ensure PLIST is a property list, resolving layer names and handling reactive vars.
If PLIST is a symbol, resolve it via `tp--resolve-props'.
If PLIST is a plist, also process it via `tp--resolve-props' to handle
anonymous reactive layers.
If resolution fails, return PLIST unchanged (for backward compatibility)."
(if (symbolp plist)
(or (tp--resolve-props plist) plist)
plist))
(or (tp--resolve-props plist) plist))
(defun tp-layer-reset ()
"Reset all layer definitions.