Convert tp-define-layer and tp-define-layer-group to functions with quoted args

- Convert tp-define-layer from macro to function that accepts evaluated args
- Add define-tp macro wrapper for convenient syntax
- Convert tp-define-layer-group from macro to function that accepts evaluated args
- Add define-tp-group macro wrapper for convenient syntax
- Update all tests to use new quoted format

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-12-21 02:16:45 +00:00
parent cedacc6b20
commit 7302795530
2 changed files with 387 additions and 417 deletions

File diff suppressed because it is too large Load Diff

336
tp.el
View File

@ -1976,9 +1976,13 @@ Returns a plist of all properties in the region or string."
;;; Layer Definition Functions ;;; Layer Definition Functions
(defun tp--parse-define-layer-args (args) (defun tp--parse-define-layer-args (args)
"Parse ARGS for tp-define-layer macro. "Parse ARGS for tp-define-layer function.
Returns a plist with keys :props, :data, :watch, :compute. Returns a plist with keys :props, :data, :watch, :compute.
When :watch, :compute, or :data are present, :props is required." When :watch, :compute, or :data are present, :props is required.
ARGS can be:
- A single plist: the properties directly
- Keyword arguments: :props PLIST [:data DATA] [:watch WATCH] [:compute COMPUTE]"
(let (props data watch compute has-keywords) (let (props data watch compute has-keywords)
(cond (cond
;; Check for keyword arguments format ;; Check for keyword arguments format
@ -1997,36 +2001,32 @@ When :watch, :compute, or :data are present, :props is required."
;; Validate: if :watch, :compute, or :data present, :props must be present ;; Validate: if :watch, :compute, or :data present, :props must be present
(when (and (or watch compute data) (null props)) (when (and (or watch compute data) (null props))
(error "When using :watch, :compute, or :data, :props must be explicitly specified"))) (error "When using :watch, :compute, or :data, :props must be explicitly specified")))
;; Format 1: single plist (legacy) ;; Format 1: single plist (the plist directly as first arg)
((and (= (length args) 1) ((and (= (length args) 1)
(listp (car args))) (listp (car args)))
(setq props (car args))) (setq props (car args)))
(t (error "Invalid tp-define-layer format"))) (t (error "Invalid tp-define-layer format")))
(list :props props :data data :watch watch :compute compute))) (list :props props :data data :watch watch :compute compute)))
(defmacro tp-define-layer (name &rest args) (defun tp-define-layer (name &rest args)
"Define a single text property layer named NAME. "Define a single text property layer named NAME.
This macro supports three formats: This function supports two formats:
Format 1 - Direct plist (legacy, no :watch/:compute/:data support): Format 1 - Direct plist (no :watch/:compute/:data support):
(tp-define-layer layer-name (tp-define-layer \\='layer-name
(display \"🌑\" face (:height 1.0))) \\='(display \"🌑\" face (:height 1.0)))
Format 2 - With :props keyword: Format 2 - With :props, :data, :watch, and/or :compute (Vue 3 style reactivity):
(tp-define-layer layer-name (tp-define-layer \\='layer-name
:props (display \"🌑\" face (:height 1.0)))
Format 3 - With :props, :data, :watch, and/or :compute (Vue 3 style reactivity):
(tp-define-layer layer-name
;; props: $-prefixed symbols are reactive variables; auto-defined if not bound ;; props: $-prefixed symbols are reactive variables; auto-defined if not bound
:props (face (:foreground $my-color) help-echo $full-name) :props \\='(face (:foreground $my-color) help-echo $full-name)
;; data: additional reactive variables not used in props; auto-defined if not bound ;; data: additional reactive variables not used in props; auto-defined if not bound
:data (first-name last-name) :data \\='((first-name . \"John\") (last-name . \"Doe\"))
;; compute: list of (VAR-NAME FUNCTION) - compute reactive variable values ;; compute: list of (VAR-NAME FUNCTION) - compute reactive variable values
:compute ((full-name (lambda () (concat first-name \" \" last-name)))) :compute \\='((full-name (lambda () (concat first-name \" \" last-name))))
;; watch: list of (VAR-NAME CALLBACK) - side effects when vars change ;; watch: list of (VAR-NAME CALLBACK) - side effects when vars change
:watch ((my-color (lambda (new old layer) :watch \\='((my-color (lambda (new old layer)
(message \"Color changed from %s to %s\" old new))))) (message \"Color changed from %s to %s\" old new)))))
Reactive Variables: Reactive Variables:
@ -2034,7 +2034,8 @@ Reactive Variables:
Variables in :data are also reactive. All reactive variables are automatically Variables in :data are also reactive. All reactive variables are automatically
defined as global variables if they are not already bound. defined as global variables if they are not already bound.
:data - A list of variable symbols for additional reactive state not in :props. :data - A list of variable symbols or cons cells (SYMBOL . INITIAL-VALUE)
for additional reactive state not in :props.
:compute - A list of (VAR-SYMBOL COMPUTE-FN) pairs. COMPUTE-FN is evaluated :compute - A list of (VAR-SYMBOL COMPUTE-FN) pairs. COMPUTE-FN is evaluated
to compute the value of VAR-SYMBOL. Can reference other reactive variables to compute the value of VAR-SYMBOL. Can reference other reactive variables
@ -2048,7 +2049,6 @@ the text properties explicitly.
If a layer with the same NAME already exists, it will be overwritten. If a layer with the same NAME already exists, it will be overwritten.
The layer is stored in `tp-layer-alist'." The layer is stored in `tp-layer-alist'."
(declare (indent defun))
(let* ((parsed (tp--parse-define-layer-args args)) (let* ((parsed (tp--parse-define-layer-args args))
(properties (plist-get parsed :props)) (properties (plist-get parsed :props))
(data (plist-get parsed :data)) (data (plist-get parsed :data))
@ -2071,41 +2071,60 @@ The layer is stored in `tp-layer-alist'."
computed-vars)))) computed-vars))))
(if (or all-reactive-syms data compute) (if (or all-reactive-syms data compute)
;; Has reactive features - register dependencies and resolve at runtime ;; Has reactive features - register dependencies and resolve at runtime
`(progn (progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition) ;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition)
(tp--unregister-reactive-deps ',name) (tp--unregister-reactive-deps name)
;; Ensure all reactive variables are defined ;; Ensure all reactive variables are defined
(tp--ensure-reactive-variables ',all-vars-to-define) (tp--ensure-reactive-variables all-vars-to-define)
;; Register data variables ;; Register data variables
,@(when data (when data
`((tp--register-layer-data ',name ',data))) (tp--register-layer-data name data))
;; Register computed variable definitions ;; Register computed variable definitions
,@(when compute (when compute
`((tp--register-layer-computed ',name ',compute) (tp--register-layer-computed name compute)
;; Apply initial computed values ;; Apply initial computed values
(tp--apply-initial-computed ',compute))) (tp--apply-initial-computed compute))
;; Register reactive dependencies ;; Register reactive dependencies
(tp--register-reactive-deps ',name ',all-reactive-syms ',properties) (tp--register-reactive-deps name all-reactive-syms properties)
;; Register watchers ;; Register watchers
,@(when watch (when watch
`((tp--register-layer-watchers ',name ',watch))) (tp--register-layer-watchers name watch))
;; Set layer properties with resolved values ;; Set layer properties with resolved values
(let ((resolved-props (tp--resolve-reactive-symbols ',properties))) (let ((resolved-props (tp--resolve-reactive-symbols properties)))
(tp--set-layer-props ',name resolved-props)) (tp--set-layer-props name resolved-props))
;; Update any text regions that already have this layer applied ;; Update any text regions that already have this layer applied
;; This ensures re-definition immediately updates applied text ;; This ensures re-definition immediately updates applied text
(tp--update-layer-regions ',name) (tp--update-layer-regions name)
(assoc ',name tp-layer-alist)) (assoc name tp-layer-alist))
;; No reactive symbols - use static properties ;; No reactive symbols - use static properties
`(progn (progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition) ;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition)
(tp--unregister-reactive-deps ',name) (tp--unregister-reactive-deps name)
(tp--set-layer-props ',name ',properties) (tp--set-layer-props name properties)
;; Update any text regions that already have this layer applied ;; Update any text regions that already have this layer applied
(tp--update-layer-regions ',name) (tp--update-layer-regions name)
(assoc ',name tp-layer-alist))))) (assoc name tp-layer-alist)))))
(defalias 'define-tp 'tp-define-layer) (defmacro define-tp (name &rest args)
"Define a single text property layer named NAME.
This macro provides a convenient syntax for `tp-define-layer'.
All arguments use quoted list format.
Format 1 - Direct plist (no :watch/:compute/:data support):
(define-tp tp-process-bar-done
\\='(display (space :width ($tp-done-pixel))
face (:background $tp-done-color)))
Format 2 - With :props, :data, :watch, and/or :compute:
(define-tp tp-process-bar-done
:props \\='(display (space :width ($tp-done-pixel))
face (:background $tp-done-color))
:data \\=`((tp-done-color . ,tp-success-color)))
See `tp-define-layer' for full documentation."
(declare (indent defun))
`(tp-define-layer ',name ,@args))
(defun tp--layer-group-element-format (element) (defun tp--layer-group-element-format (element)
"Determine the format type of ELEMENT. "Determine the format type of ELEMENT.
@ -2188,73 +2207,14 @@ For format-4 elements, returns (LAYER-NAME :props PROPS :data DATA :watch WATCH
(cons layer-name element))) (cons layer-name element)))
(_ (error "Invalid layer group element: %S" element))))) (_ (error "Invalid layer group element: %S" element)))))
(defmacro tp-define-layer-group (name &rest elements) (defun tp--define-layer-from-parsed (layer-name props data watch compute)
"Define a layer group named NAME containing multiple layers. "Internal helper to define a layer from parsed components.
LAYER-NAME is the symbol name for the layer.
This macro supports four formats for each element: PROPS is the property list.
DATA is the list of data variables.
Format 1 - Anonymous layers (named as NAME-0, NAME-1, etc.): WATCH is the list of watcher definitions.
(tp-define-layer-group group-name COMPUTE is the list of computed variable definitions."
(display \"🌑\" face (:height 1.0)) (let* ((reactive-syms (tp--collect-reactive-symbols props))
(display \"🌘\" face (:height 1.5))
(display \"🌗\" face (:height 2.0)))
Format 2 - Named layers with cons-cell (named as NAME-suffix):
(tp-define-layer-group group-name
(\"新月\" . (display \"🌑\" face (:height 1.0)))
(\"残月\" . (display \"🌘\" face (:height 1.5)))
(\"下弦月\" . (display \"🌗\" face (:height 2.0))))
Format 3 - Named layers with :props keyword (named as NAME-suffix):
(tp-define-layer-group group-name
(\"新月\" :props (display \"🌑\" face (:height 1.0)))
(\"残月\" :props (display \"🌘\" face (:height 1.5)))
(\"下弦月\" :props (display \"🌗\" face (:height 2.0))))
Format 4 - Named layers with :props, :data, :watch, and/or :compute (Vue 3 style):
(tp-define-layer-group group-name
(\"reactive\" :props (face (:foreground $my-color) help-echo $full-name)
:data (first-name last-name)
:compute ((full-name (lambda () (concat first-name \" \" last-name))))
:watch ((my-color (lambda (new old layer) (message \"Changed!\"))))))
Reactive Variables:
If any symbol in :props starts with $, it is treated as a reactive variable.
Variables in :data are also reactive. All reactive variables are automatically
defined as global variables if they are not already bound.
:data - A list of variable symbols for additional reactive state not in :props.
:compute - A list of (VAR-SYMBOL COMPUTE-FN) pairs for computed reactive variables.
:watch - A list of (VAR-SYMBOL CALLBACK) pairs for side effects.
You can also reference already-defined layers by their symbol name:
(tp-define-layer-group group-name
existing-layer-1
existing-layer-2
(display \"🌗\" face (:height 2.0)))
If a layer group with the same NAME already exists, it will be overwritten.
Individual layers created by the group are stored in `tp-layer-alist',
and the group itself is stored in `tp-layer-groups'."
(declare (indent defun))
(let ((layer-names nil)
(layer-defs nil)
(idx 0))
(dolist (element elements)
(let ((parsed (tp--parse-layer-group-element name element idx)))
(cond
;; Reference to existing layer (symbol)
((symbolp parsed)
(push parsed layer-names))
;; Extended format with :data/:watch/:compute (format-4)
;; parsed is (layer-name :props ... :data ... :watch ... :compute ...)
((and (listp parsed) (plist-get (cdr parsed) :props))
(let* ((layer-name (car parsed))
(props (plist-get (cdr parsed) :props))
(data (plist-get (cdr parsed) :data))
(watch (plist-get (cdr parsed) :watch))
(compute (plist-get (cdr parsed) :compute))
(reactive-syms (tp--collect-reactive-symbols props))
(computed-vars (when compute (mapcar #'car compute))) (computed-vars (when compute (mapcar #'car compute)))
(all-reactive-syms (delete-dups reactive-syms)) (all-reactive-syms (delete-dups reactive-syms))
(props-vars (mapcar #'tp--reactive-var-symbol reactive-syms)) (props-vars (mapcar #'tp--reactive-var-symbol reactive-syms))
@ -2264,78 +2224,116 @@ and the group itself is stored in `tp-layer-groups'."
computed-vars)))) computed-vars))))
(if (or all-reactive-syms data compute) (if (or all-reactive-syms data compute)
;; Has reactive features - register dependencies and resolve at runtime ;; Has reactive features - register dependencies and resolve at runtime
(push `(progn (progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition) (tp--unregister-reactive-deps layer-name)
(tp--unregister-reactive-deps ',layer-name) (tp--ensure-reactive-variables all-vars-to-define)
;; Ensure all reactive variables are defined (when data
(tp--ensure-reactive-variables ',all-vars-to-define) (tp--register-layer-data layer-name data))
;; Register data variables (when compute
,@(when data (tp--register-layer-computed layer-name compute)
`((tp--register-layer-data ',layer-name ',data))) (tp--apply-initial-computed compute))
;; Register computed variable definitions (tp--register-reactive-deps layer-name all-reactive-syms props)
,@(when compute (when watch
`((tp--register-layer-computed ',layer-name ',compute) (tp--register-layer-watchers layer-name watch))
(tp--apply-initial-computed ',compute))) (let ((resolved-props (tp--resolve-reactive-symbols props)))
;; Register reactive dependencies (tp--set-layer-props layer-name resolved-props))
(tp--register-reactive-deps (tp--update-layer-regions layer-name))
',layer-name ',all-reactive-syms ',props)
;; Register watchers
,@(when watch
`((tp--register-layer-watchers ',layer-name ',watch)))
;; Set layer properties with resolved values
(let ((resolved-props (tp--resolve-reactive-symbols ',props)))
(tp--set-layer-props ',layer-name resolved-props))
;; Update any text regions that already have this layer applied
(tp--update-layer-regions ',layer-name))
layer-defs)
;; No reactive symbols - use static properties ;; No reactive symbols - use static properties
(push `(progn (progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition) (tp--unregister-reactive-deps layer-name)
(tp--unregister-reactive-deps ',layer-name) (tp--set-layer-props layer-name props)
(tp--set-layer-props ',layer-name ',props) (tp--update-layer-regions layer-name)))
;; Update any text regions that already have this layer applied layer-name))
(tp--update-layer-regions ',layer-name))
layer-defs)) (defun tp-define-layer-group (name &rest elements)
"Define a layer group named NAME containing multiple layers.
This function accepts a list of layer definitions in ELEMENTS.
Each element in ELEMENTS should be one of:
- A symbol: reference to an existing layer
- A plist: anonymous layer (named as NAME-0, NAME-1, etc.)
- A cons cell (\"suffix\" . plist): named layer (named as NAME-suffix)
- A list (\"suffix\" :props plist [:data data] [:watch watch] [:compute compute]):
named layer with reactive features
All property lists should be evaluated (quoted in the call).
Example:
(tp-define-layer-group \\='my-group
\\='existing-layer
\\='(face bold)
\\='(\"named\" . (face italic))
\\='(\"reactive\" :props (face (:foreground $color))
:data ((color . \"red\"))))
If a layer group with the same NAME already exists, it will be overwritten.
Individual layers created by the group are stored in `tp-layer-alist',
and the group itself is stored in `tp-layer-groups'."
(let ((layer-names nil)
(idx 0))
(dolist (element elements)
(let ((parsed (tp--parse-layer-group-element name element idx)))
(cond
;; Reference to existing layer (symbol)
((symbolp parsed)
(push parsed layer-names))
;; Extended format with :data/:watch/:compute (format-4)
((and (listp parsed) (plist-get (cdr parsed) :props))
(let* ((layer-name (car parsed))
(props (plist-get (cdr parsed) :props))
(data (plist-get (cdr parsed) :data))
(watch (plist-get (cdr parsed) :watch))
(compute (plist-get (cdr parsed) :compute)))
(tp--define-layer-from-parsed layer-name props data watch compute)
(push layer-name layer-names))) (push layer-name layer-names)))
;; Simple format (cons cell of name . props) ;; Simple format (cons cell of name . props)
((consp parsed) ((consp parsed)
(let* ((layer-name (car parsed)) (let* ((layer-name (car parsed))
(props (cdr parsed)) (props (cdr parsed)))
(reactive-syms (tp--collect-reactive-symbols props))) (tp--define-layer-from-parsed layer-name props nil nil nil)
(if reactive-syms
;; Has reactive symbols - register dependencies and resolve at runtime
(push `(progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition)
(tp--unregister-reactive-deps ',layer-name)
(tp--ensure-reactive-variables
',(mapcar #'tp--reactive-var-symbol reactive-syms))
(tp--register-reactive-deps
',layer-name ',reactive-syms ',props)
(let ((resolved-props (tp--resolve-reactive-symbols ',props)))
(tp--set-layer-props ',layer-name resolved-props))
;; Update any text regions that already have this layer applied
(tp--update-layer-regions ',layer-name))
layer-defs)
;; No reactive symbols - use static properties
(push `(progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition)
(tp--unregister-reactive-deps ',layer-name)
(tp--set-layer-props ',layer-name ',props)
;; Update any text regions that already have this layer applied
(tp--update-layer-regions ',layer-name))
layer-defs))
(push layer-name layer-names) (push layer-name layer-names)
;; Only increment idx for anonymous (Format 1) elements ;; Only increment idx for anonymous (Format 1) elements
(when (eq (tp--layer-group-element-format element) 'format-1) (when (eq (tp--layer-group-element-format element) 'format-1)
(cl-incf idx))))))) (cl-incf idx)))))))
(setq layer-names (nreverse layer-names)) (setq layer-names (nreverse layer-names))
(setq layer-defs (nreverse layer-defs)) (tp--set-group-layers name layer-names)
`(progn (assoc name tp-layer-groups)))
,@layer-defs
(tp--set-group-layers ',name ',layer-names)
(assoc ',name tp-layer-groups))))
(defalias 'define-tp-group 'tp-define-layer-group) (defmacro define-tp-group (name &rest elements)
"Define a layer group named NAME containing multiple layers.
This macro provides a convenient syntax for `tp-define-layer-group'.
All layer definitions should use quoted list format.
Supported formats for each element in ELEMENTS:
Format 1 - Existing layer reference:
\\='existing-layer-name
Format 2 - Anonymous layer (named as NAME-0, NAME-1, etc.):
\\='(display \"🌑\" face (:height 1.0))
Format 3 - Named layer with cons-cell (named as NAME-suffix):
\\='(\"新月\" . (display \"🌑\" face (:height 1.0)))
Format 4 - Named layer with :props keyword (named as NAME-suffix):
\\='(\"新月\" :props (display \"🌑\" face (:height 1.0)))
Format 5 - Named layer with :props, :data, :watch, and/or :compute:
\\='(\"reactive\" :props (face (:foreground $my-color))
:data ((my-color . \"red\"))
:watch ((my-color (lambda (new old layer) (message \"Changed!\")))))
Example:
(define-tp-group my-group
\\='existing-layer
\\='(face bold)
\\='(\"named\" . (face italic)))
See `tp-define-layer-group' for full documentation."
(declare (indent defun))
`(tp-define-layer-group ',name ,@elements))
(defun tp--set-layer-props (layer-name properties) (defun tp--set-layer-props (layer-name properties)
"Set PROPERTIES for layer LAYER-NAME in `tp-layer-alist'. "Set PROPERTIES for layer LAYER-NAME in `tp-layer-alist'.