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
(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.
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)
(cond
;; 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
(when (and (or watch compute data) (null props))
(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)
(listp (car args)))
(setq props (car args)))
(t (error "Invalid tp-define-layer format")))
(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.
This macro supports three formats:
This function supports two formats:
Format 1 - Direct plist (legacy, no :watch/:compute/:data support):
(tp-define-layer layer-name
(display \"🌑\" face (:height 1.0)))
Format 1 - Direct plist (no :watch/:compute/:data support):
(tp-define-layer \\='layer-name
\\='(display \"🌑\" face (:height 1.0)))
Format 2 - With :props keyword:
(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
Format 2 - 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 (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 (first-name last-name)
:data \\='((first-name . \"John\") (last-name . \"Doe\"))
;; 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 ((my-color (lambda (new old layer)
:watch \\='((my-color (lambda (new old layer)
(message \"Color changed from %s to %s\" old new)))))
Reactive Variables:
@ -2034,7 +2034,8 @@ Reactive Variables:
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.
: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
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.
The layer is stored in `tp-layer-alist'."
(declare (indent defun))
(let* ((parsed (tp--parse-define-layer-args args))
(properties (plist-get parsed :props))
(data (plist-get parsed :data))
@ -2071,41 +2071,60 @@ The layer is stored in `tp-layer-alist'."
computed-vars))))
(if (or all-reactive-syms data compute)
;; Has reactive features - register dependencies and resolve at runtime
`(progn
(progn
;; 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
(tp--ensure-reactive-variables ',all-vars-to-define)
(tp--ensure-reactive-variables all-vars-to-define)
;; Register data variables
,@(when data
`((tp--register-layer-data ',name ',data)))
(when data
(tp--register-layer-data name data))
;; Register computed variable definitions
,@(when compute
`((tp--register-layer-computed ',name ',compute)
(when compute
(tp--register-layer-computed name compute)
;; Apply initial computed values
(tp--apply-initial-computed ',compute)))
(tp--apply-initial-computed compute))
;; Register reactive dependencies
(tp--register-reactive-deps ',name ',all-reactive-syms ',properties)
(tp--register-reactive-deps name all-reactive-syms properties)
;; Register watchers
,@(when watch
`((tp--register-layer-watchers ',name ',watch)))
(when watch
(tp--register-layer-watchers name watch))
;; Set layer properties with resolved values
(let ((resolved-props (tp--resolve-reactive-symbols ',properties)))
(tp--set-layer-props ',name resolved-props))
(let ((resolved-props (tp--resolve-reactive-symbols properties)))
(tp--set-layer-props name resolved-props))
;; Update any text regions that already have this layer applied
;; This ensures re-definition immediately updates applied text
(tp--update-layer-regions ',name)
(assoc ',name tp-layer-alist))
(tp--update-layer-regions name)
(assoc name tp-layer-alist))
;; No reactive symbols - use static properties
`(progn
(progn
;; Clean up old reactive dependencies, watchers, computed properties, and data (for re-definition)
(tp--unregister-reactive-deps ',name)
(tp--set-layer-props ',name ',properties)
(tp--unregister-reactive-deps name)
(tp--set-layer-props name properties)
;; Update any text regions that already have this layer applied
(tp--update-layer-regions ',name)
(assoc ',name tp-layer-alist)))))
(tp--update-layer-regions name)
(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)
"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)))
(_ (error "Invalid layer group element: %S" element)))))
(defmacro tp-define-layer-group (name &rest elements)
"Define a layer group named NAME containing multiple layers.
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
(display \"🌑\" face (:height 1.0))
(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))
(defun tp--define-layer-from-parsed (layer-name props data watch compute)
"Internal helper to define a layer from parsed components.
LAYER-NAME is the symbol name for the layer.
PROPS is the property list.
DATA is the list of data variables.
WATCH is the list of watcher definitions.
COMPUTE is the list of computed variable definitions."
(let* ((reactive-syms (tp--collect-reactive-symbols props))
(computed-vars (when compute (mapcar #'car compute)))
(all-reactive-syms (delete-dups 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))))
(if (or all-reactive-syms data compute)
;; Has reactive features - 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)
;; Ensure all reactive variables are defined
(tp--ensure-reactive-variables ',all-vars-to-define)
;; Register data variables
,@(when data
`((tp--register-layer-data ',layer-name ',data)))
;; Register computed variable definitions
,@(when compute
`((tp--register-layer-computed ',layer-name ',compute)
(tp--apply-initial-computed ',compute)))
;; Register reactive dependencies
(tp--register-reactive-deps
',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)
(progn
(tp--unregister-reactive-deps layer-name)
(tp--ensure-reactive-variables all-vars-to-define)
(when data
(tp--register-layer-data layer-name data))
(when compute
(tp--register-layer-computed layer-name compute)
(tp--apply-initial-computed compute))
(tp--register-reactive-deps layer-name all-reactive-syms props)
(when watch
(tp--register-layer-watchers layer-name watch))
(let ((resolved-props (tp--resolve-reactive-symbols props)))
(tp--set-layer-props layer-name resolved-props))
(tp--update-layer-regions layer-name))
;; 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))
(progn
(tp--unregister-reactive-deps layer-name)
(tp--set-layer-props layer-name props)
(tp--update-layer-regions layer-name)))
layer-name))
(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)))
;; Simple format (cons cell of name . props)
((consp parsed)
(let* ((layer-name (car parsed))
(props (cdr parsed))
(reactive-syms (tp--collect-reactive-symbols props)))
(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))
(props (cdr parsed)))
(tp--define-layer-from-parsed layer-name props nil nil nil)
(push layer-name layer-names)
;; Only increment idx for anonymous (Format 1) elements
(when (eq (tp--layer-group-element-format element) 'format-1)
(cl-incf idx)))))))
(setq layer-names (nreverse layer-names))
(setq layer-defs (nreverse layer-defs))
`(progn
,@layer-defs
(tp--set-group-layers ',name ',layer-names)
(assoc ',name tp-layer-groups))))
(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)
"Set PROPERTIES for layer LAYER-NAME in `tp-layer-alist'.