From 56733e747343fadfa58f3cb2289b745848a77b99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 20 Dec 2025 07:51:27 +0000 Subject: [PATCH] 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> --- tp-tests.el | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tp.el | 66 +++++++++++++++++++++++++++++-------- 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/tp-tests.el b/tp-tests.el index 41fa7d3..83abc53 100644 --- a/tp-tests.el +++ b/tp-tests.el @@ -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 diff --git a/tp.el b/tp.el index 5224312..9368859 100644 --- a/tp.el +++ b/tp.el @@ -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.