diff --git a/README.md b/README.md index cb873cd..694d6e7 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ - [Property Layer Definition](#property-layer-definition) - [define-tp / define-tps](#define-tp--define-tps---define-custom-text-properties) - [tp-layer-props / tp-group-props](#tp-layer-props--tp-group-props) + - [tp-layer-props-with-args / tp-group-props-with-args / tp-layer-arglist](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) + - [tp-describe-layer](#tp-describe-layer---describe-a-layer) - [tp-undefine-layer / tp-undefine-group](#tp-undefine-layer--tp-undefine-group) - [tp-layer-reset](#tp-layer-reset) - [tp-reactive-reset](#tp-reactive-reset) @@ -74,9 +76,12 @@ - [Property Layer Movement](#property-layer-movement) - [tp-move-layer](#tp-move-layer---move-layer-to-position) - [tp-raise-layer](#tp-raise-layer---move-layer-updown) + - [tp-lower-layer](#tp-lower-layer---mirror-of-tp-raise-layer) - [tp-rotate-layer](#tp-rotate-layer---cycle-layers) - [tp-pin-layer](#tp-pin-layer---pin-layer-to-top) - [tp-switch-layer](#tp-switch-layer---switch-two-layers) + - [Property Layer Visibility](#property-layer-visibility) + - [tp-hide-layer / tp-show-layer](#tp-hide-layer--tp-show-layer---hide-and-show-layers) - [Property Layer Merging](#property-layer-merging) - [tp-merge-layers](#tp-merge-layers---merge-multiple-layers) - [tp-flatten-layers](#tp-flatten-layers---flatten-all-layers) @@ -85,6 +90,7 @@ - [tp-layer-count](#tp-layer-count) - [tp-layer-exists-p](#tp-layer-exists-p) - [tp-layer-top](#tp-layer-top) + - [tp-layer-stack-at](#tp-layer-stack-at---full-stack-at-a-position) - [tp-add-to-layers](#tp-add-to-layers---add-properties-to-specific-layers) - [tp-add-to-all-layers](#tp-add-to-all-layers---add-properties-to-all-layers) - [Utility Functions](#utility-functions) @@ -107,6 +113,7 @@ - [Layer Name Resolution in APIs](#layer-name-resolution-in-apis) - [Reactive Layer Groups](#reactive-layer-groups) - [Batched Updates](#batched-updates) + - [Layer-Buffer Registry & Lifecycle](#layer-buffer-registry--lifecycle) - [Debug Mode](#debug-mode) - [Resetting Reactive State](#resetting-reactive-state) - [Complete Example: Theme-Aware Text](#complete-example-theme-aware-text) @@ -185,6 +192,43 @@ Native Emacs APIs have different functions and parameter orders for strings and ``` - ✅ **Unified Object Support**: The same function works with both strings and buffers, no need to remember different APIs +**One rule to remember**: when the first argument is a **string**, the call +operates on that whole string; when it is a **number**, the call operates on +the `[START, END)` region of OBJECT — and OBJECT always comes last (nil means +the current buffer). Every core and layer-stack function follows this rule. + +The match/search family (`tp-match-*`, `tp-regexp-*`, `tp-search-map`, +`tp-forward-do`/`tp-backward-do`) follows a deliberate **second convention**: +PATTERN (or FUNCTION) and PLIST come first, then OBJECT, then the optional +START/END bounds. Operating on the whole object is these functions' common +case, so OBJECT sits before the range instead of after it. + +**Return value conventions** (as of 0.3.0): + +| Family | Return value | +|---|---| +| `tp-set` / `tp-reset` / `tp-add` | `(START . END)` for buffer/region forms; a **new** string for whole-string forms | +| `tp-remove` | nil for buffer forms; a **new** string for whole-string forms | +| `tp-clear` | nil | +| `tp-match-*` / `tp-regexp-*` | list of `(START . END)` matches for buffers; a **new** string for strings | +| Stack mutators (delete/pop/move/raise/lower/rotate/pin/switch/hide/show/merge/flatten) | the number of property runs modified (0 = nothing matched) | +| `tp-put-layer` / `tp-push-layer` | OBJECT when given (the string itself in string forms), else `(START . END)` | +| `tp-add-to-layers` / `tp-add-to-all-layers` | the string itself (mutated **in place**) for string forms; nil for buffers | + +**Namespace map**: `tp-layer-NAME` functions taking a *layer name* argument +(`tp-layer-props`, `tp-layer-arglist`, ...) query the layer **registry** +(definitions); the ones taking *position* arguments — START END +(`tp-layer-list`, `tp-layer-count`, `tp-layer-top`, ...) or a single POS +(`tp-layer-stack-at`) — query the layer **stack on actual text**. + +**Naming conventions**: `tp-define-layer` / `tp-define-group` / +`tp-define-palette` are the prefix-conforming canonical names going forward +(discoverable via `C-h f tp-...`); `define-tp` / `define-tps` / +`define-tp-group` / `define-tp-palette` are permanent aliases that will never +be removed (this README's examples still use the historical names). +`tp-search-forward` / `tp-search-backward` are deprecated since 0.3.0 — see +[Search & Navigation](#tp-search-forward--tp-search-backward). + ### Three Property Operation Semantics Native APIs only have simple set and get. tp.el provides three clear operation semantics: @@ -255,9 +299,10 @@ Native APIs only have simple set and get. tp.el provides three clear operation s - ✅ **Rich Property Layer Operations**: - Placement: `tp-put-layer` (specific position), `tp-push-layer` (top) - Deletion: `tp-delete-layer` (by name/index), `tp-pop-layer` (top layer) - - Movement: `tp-raise-layer` (up/down), `tp-rotate-layer` (rotate), `tp-pin-layer` (pin to top), `tp-switch-layer` (swap) + - Movement: `tp-raise-layer` / `tp-lower-layer` (up/down), `tp-rotate-layer` (rotate), `tp-pin-layer` (one-shot move to top), `tp-switch-layer` (swap) + - Visibility: `tp-hide-layer` / `tp-show-layer` (hide a layer without removing it) - Merging: `tp-merge-layers` (merge specified layers), `tp-flatten-layers` (flatten all layers) -- ✅ **Property Layer Queries**: `tp-layer-list`, `tp-layer-count`, `tp-layer-exists-p`, `tp-layer-top` +- ✅ **Property Layer Queries**: `tp-layer-list`, `tp-layer-count`, `tp-layer-exists-p`, `tp-layer-top`, `tp-layer-stack-at` ```elisp ;; Property layer usage example @@ -300,6 +345,7 @@ Native APIs require manual searching and looping. tp.el provides convenient patt - ✅ **:data for Additional State**: Define additional reactive variables that aren't directly used in properties but can trigger updates - ✅ **:compute for Derived Values**: Create computed properties that derive their values from other reactive variables (like Vue's computed properties) - ✅ **:watch for Side Effects**: Execute callbacks when reactive variables change (like Vue's watch) +- ✅ **Targeted Updates (0.3.0)**: a layer→buffer registry means updates visit only the buffers showing the affected layer, `tp-text` re-renders edit only the differing span (point and markers stay put), and `tp-reactive-track-buffer` / `tp-gc-anonymous-layers` manage the layer lifecycle — see [Layer-Buffer Registry & Lifecycle](#layer-buffer-registry--lifecycle) ```elisp ;; Define a layer with reactive properties @@ -327,8 +373,8 @@ Native APIs require manual searching and looping. tp.el provides convenient patt ### Enhanced Search & Navigation - ✅ **Range Search**: `tp-search` returns a list of all matching intervals -- ✅ **N-times Search**: `tp-forward`/`tp-backward` support searching forward/backward N times -- ✅ **Search and Execute**: `tp-forward-do`/`tp-backward-do` search and execute function on matched text +- ✅ **N-times Search**: `tp-forward`/`tp-backward` support searching forward/backward N times, with optional PREDICATE matching and NOT-CURRENT +- ✅ **Search and Execute**: `tp-forward-do`/`tp-backward-do` search N times and apply a function at the Nth match - ✅ **Batch Transform**: `tp-search-map` applies transformation function to all matches ```elisp @@ -405,32 +451,37 @@ A complete overview of all tp.el functions organized by category: #### Pattern Matching Functions | Function | Description | |----------|-------------| -| [`tp-match-set`](#tp-match-set---match-string) | Set properties on string pattern matches | -| [`tp-match-reset`](#tp-match-reset---match-and-reset) | Reset all properties on string matches | -| [`tp-match-add`](#tp-match-add---match-and-add) | Add/merge properties on string matches | -| [`tp-regexp-set`](#tp-regexp-set---match-regexp) | Set properties on regexp matches | -| [`tp-regexp-reset`](#tp-regexp-reset---regexp-and-reset) | Reset all properties on regexp matches | -| [`tp-regexp-add`](#tp-regexp-add---regexp-and-add) | Add/merge properties on regexp matches | +| [`tp-match-set`](#tp-match-set---match-string) | Set properties on string pattern matches (optional bounds) | +| [`tp-match-reset`](#tp-match-reset---match-and-reset) | Reset all properties on string matches (optional bounds) | +| [`tp-match-add`](#tp-match-add---match-and-add) | Add/merge properties on string matches (optional bounds) | +| [`tp-regexp-set`](#tp-regexp-set---match-regexp) | Set properties on regexp matches (optional bounds and capture group) | +| [`tp-regexp-reset`](#tp-regexp-reset---regexp-and-reset) | Reset all properties on regexp matches (optional bounds and capture group) | +| [`tp-regexp-add`](#tp-regexp-add---regexp-and-add) | Add/merge properties on regexp matches (optional bounds and capture group) | #### Search & Navigation Functions | Function | Description | |----------|-------------| -| [`tp-search-forward`](#tp-search-forward--tp-search-backward) | Raw wrapper for text-property-search-forward | -| [`tp-search-backward`](#tp-search-forward--tp-search-backward) | Raw wrapper for text-property-search-backward | -| [`tp-forward`](#tp-forward--tp-backward) | Search forward N times for text with property (buffers and strings) | -| [`tp-backward`](#tp-forward--tp-backward) | Search backward N times for text with property (buffers and strings) | -| [`tp-forward-do`](#tp-forward-do--tp-backward-do) | Apply function to last match in forward search (with optional start/end range) | -| [`tp-backward-do`](#tp-forward-do--tp-backward-do) | Apply function to last match in backward search (with optional start/end range) | +| [`tp-search-forward`](#tp-search-forward--tp-search-backward) | **Deprecated (0.3.0)** — use [`tp-forward`](#tp-forward--tp-backward) or the Emacs primitive | +| [`tp-search-backward`](#tp-search-forward--tp-search-backward) | **Deprecated (0.3.0)** — use [`tp-backward`](#tp-forward--tp-backward) or the Emacs primitive | +| [`tp-forward`](#tp-forward--tp-backward) | Search forward N times for text with property (optional predicate matching) | +| [`tp-backward`](#tp-forward--tp-backward) | Search backward N times for text with property (optional predicate matching) | +| [`tp-forward-do`](#tp-forward-do--tp-backward-do) | Search forward N times, apply function at the Nth match | +| [`tp-backward-do`](#tp-forward-do--tp-backward-do) | Search backward N times, apply function at the Nth match | | [`tp-search`](#tp-search---search-all-matches) | Search all matching properties in range or string | | [`tp-search-map`](#tp-search-map---apply-function-to-matched-text) | Apply function to all matches (with optional start/end range) | #### Property Layer Definition Functions | Function | Description | |----------|-------------| -| [`define-tp`](#define-tp--define-tps---define-custom-text-properties) | Define custom text property (layer) with optional parameter | -| [`define-tps`](#define-tp--define-tps---define-custom-text-properties) | Define custom text property group (layer group) with optional parameter | +| [`define-tp`](#define-tp--define-tps---define-custom-text-properties) | Define custom text property (layer) with optional parameters | +| [`define-tps`](#define-tp--define-tps---define-custom-text-properties) | Define custom text property group (layer group) with optional parameters | +| [`tp-define-layer` / `tp-define-group`](#define-tp--define-tps---define-custom-text-properties) | Prefix-conforming aliases of `define-tp` / `define-tps` | | [`tp-layer-props`](#tp-layer-props--tp-group-props) | Get properties for a layer | | [`tp-group-props`](#tp-layer-props--tp-group-props) | Get properties for all layers in a group | +| [`tp-layer-props-with-args`](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) | Expand a parameterized layer with a list of arguments | +| [`tp-group-props-with-args`](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) | Expand a parameterized group with a list of arguments | +| [`tp-layer-arglist`](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) | Get a parameterized layer's parameter list | +| [`tp-describe-layer`](#tp-describe-layer---describe-a-layer) | Describe a layer's definition in a help buffer | | [`tp-undefine-layer`](#tp-undefine-layer--tp-undefine-group) | Remove layer definition | | [`tp-undefine-group`](#tp-undefine-layer--tp-undefine-group) | Remove group definition | | [`tp-layer-reset`](#tp-layer-reset) | Clear all layer/group definitions | @@ -439,8 +490,8 @@ A complete overview of all tp.el functions organized by category: #### Property Layer Placement Functions | Function | Description | |----------|-------------| -| [`tp-put-layer`](#tp-put-layer---set-layer-at-index) | Set layer at specific index position | -| [`tp-push-layer`](#tp-push-layer---push-layer-to-top) | Push layer to top of stack | +| [`tp-put-layer`](#tp-put-layer---set-layer-at-index) | Set layer at specific index position (optional NOERROR) | +| [`tp-push-layer`](#tp-push-layer---push-layer-to-top) | Push layer to top of stack (optional NOERROR) | #### Property Layer Deletion Functions | Function | Description | @@ -453,15 +504,22 @@ A complete overview of all tp.el functions organized by category: |----------|-------------| | [`tp-move-layer`](#tp-move-layer---move-layer-to-position) | Move a layer from one position to another | | [`tp-raise-layer`](#tp-raise-layer---move-layer-updown) | Move layer up/down by N positions | -| [`tp-rotate-layer`](#tp-rotate-layer---cycle-layers) | Cycle layers (top goes to bottom) | -| [`tp-pin-layer`](#tp-pin-layer---pin-layer-to-top) | Pin a layer to top (make visible) | +| [`tp-lower-layer`](#tp-lower-layer---mirror-of-tp-raise-layer) | Mirror of `tp-raise-layer`: move layer down/up by N positions | +| [`tp-rotate-layer`](#tp-rotate-layer---cycle-layers) | Cycle layers up or down by N steps | +| [`tp-pin-layer`](#tp-pin-layer---pin-layer-to-top) | Move a layer to the top (one-shot; later pushes can cover it) | | [`tp-switch-layer`](#tp-switch-layer---switch-two-layers) | Swap positions of two layers | +#### Property Layer Visibility Functions +| Function | Description | +|----------|-------------| +| [`tp-hide-layer`](#tp-hide-layer--tp-show-layer---hide-and-show-layers) | Hide a layer without removing it from the stack | +| [`tp-show-layer`](#tp-hide-layer--tp-show-layer---hide-and-show-layers) | Make a hidden layer render again | + #### Property Layer Merging Functions | Function | Description | |----------|-------------| -| [`tp-merge-layers`](#tp-merge-layers---merge-multiple-layers) | Merge specified layers into a new layer | -| [`tp-flatten-layers`](#tp-flatten-layers---flatten-all-layers) | Flatten all layers into a single layer | +| [`tp-merge-layers`](#tp-merge-layers---merge-multiple-layers) | Merge specified layers into a new layer (hidden layers contribute no props) | +| [`tp-flatten-layers`](#tp-flatten-layers---flatten-all-layers) | Flatten all layers into a single layer (hidden layers are discarded) | #### Property Layer Query Functions | Function | Description | @@ -469,7 +527,8 @@ A complete overview of all tp.el functions organized by category: | [`tp-layer-list`](#tp-layer-list---list-all-layers) | List all layer names in region | | [`tp-layer-count`](#tp-layer-count) | Count layers in region | | [`tp-layer-exists-p`](#tp-layer-exists-p) | Check if layer exists in region | -| [`tp-layer-top`](#tp-layer-top) | Get name of top (visible) layer | +| [`tp-layer-top`](#tp-layer-top) | Get name of top layer (in stack order, even when hidden) | +| [`tp-layer-stack-at`](#tp-layer-stack-at---full-stack-at-a-position) | Full ordered stack at one position as `(NAME . PROPS)` conses | | [`tp-region-layer-props`](#tp-region-layer-props---get-layer-properties-in-region) | Get properties for a specific layer in region | #### Property Layer Manipulation Functions @@ -481,8 +540,8 @@ A complete overview of all tp.el functions organized by category: #### Utility Functions | Function | Description | |----------|-------------| -| [`tp-intervals`](#tp-intervals---get-text-property-intervals) | Get all text property intervals in a region | -| [`tp-intervals-map`](#tp-intervals-map---apply-function-to-intervals) | Apply function to all intervals in a region | +| [`tp-intervals`](#tp-intervals---get-text-property-intervals) | Get all text property intervals in a region (optional ABSOLUTE coordinates) | +| [`tp-intervals-map`](#tp-intervals-map---apply-function-to-intervals) | Apply function to all intervals in a region (optional ABSOLUTE coordinates) | | [`tp-plist`](#tp-plist---get-all-properties-in-region) | Get all properties present in a region | | [`tp-empty-p`](#tp-empty-p---check-if-object-has-properties) | Check if object has no text properties | | [`tp-with-current-buffer`](#tp-with-current-buffer--tp-pop-to-buffer--tp-switch-to-buffer) | Run body in a buffer with `inhibit-read-only` bound | @@ -493,10 +552,20 @@ A complete overview of all tp.el functions organized by category: | Function | Description | |----------|-------------| | [`tp-palette-alist`](#color-palette-system) | Registry of named palettes (variable) | -| [`define-tp-palette`](#color-palette-system) | Register or update a named palette | +| [`define-tp-palette`](#color-palette-system) | Register or update a named palette (alias: `tp-define-palette`) | +| [`tp-palette-color`](#color-palette-system) | Get a palette's `:fg` / `:bg` / `:border` color, theme-resolved | +| [`tp-palette-has-p`](#color-palette-system) | Test whether a palette (or one of its keys) is defined | | [`tp-palette-show`](#color-palette-system) | Show a gallery of all registered palettes | | [`tp-parse-color`](#color-palette-system) | Resolve a color spec for the current light/dark theme | +#### Reactive Lifecycle Functions +| Function | Description | +|----------|-------------| +| [`tp-with-batch-updates`](#batched-updates) | Apply several reactive variable changes as one update | +| [`tp-reactive-layer-buffers`](#layer-buffer-registry--lifecycle) | Buffers registered as showing a layer (or `unknown`) | +| [`tp-reactive-track-buffer`](#layer-buffer-registry--lifecycle) | Register a buffer after inserting an already-propertized string | +| [`tp-gc-anonymous-layers`](#layer-buffer-registry--lifecycle) | Collect anonymous layers no registered live buffer still shows | + --- ### Core Property Functions @@ -996,7 +1065,7 @@ Remove a property or nested sub-property from a region or entire string. (tp-clear &optional START END OBJECT) ``` -Clear all text properties from a region. +Clear all text properties from a region. Returns nil. **Examples:** @@ -1025,8 +1094,8 @@ Clear all text properties from a region. #### `tp-match-set` - Match String ```elisp -(tp-match-set PATTERN PLIST &optional OBJECT) -(tp-match-set PATTERN LAYER-NAME &optional OBJECT) +(tp-match-set PATTERN PLIST &optional OBJECT START END) +(tp-match-set PATTERN LAYER-NAME &optional OBJECT START END) ``` Set properties on all occurrences of a string pattern. @@ -1034,6 +1103,11 @@ PATTERN can be a string (single pattern) or a list of strings (multiple patterns PLIST is a property list like `'(face bold help-echo "tip")`. LAYER-NAME can be a symbol representing a layer defined by `define-tp` or a group defined by `define-tps`. OBJECT is a buffer or string; nil means current buffer. +START and END (new in 0.3.0) restrict matching to the `[START, END)` portion +of OBJECT, in native coordinates (0-based for strings, 1-based for buffers). +Matching behaves **as if OBJECT consisted only of that portion**, so no match +crosses the boundaries; reversed bounds are swapped. The same bounds are +accepted by all six `tp-match-*` / `tp-regexp-*` functions. **Examples:** @@ -1066,6 +1140,12 @@ OBJECT is a buffer or string; nil means current buffer. (insert "TODO: fix this. TODO: also this.") (tp-match-set "TODO" 'todo-style)) ;; => ((1 . 5) (17 . 21)) + +;; Restrict matching with START/END bounds - only the second TODO is in range +(with-temp-buffer + (insert "TODO one TODO two") + (tp-match-set "TODO" '(face warning) nil 5 18)) +;; => ((10 . 14)) ``` --- @@ -1079,10 +1159,13 @@ LAYER-NAME can be a symbol representing a layer defined by `define-tp` or a grou OBJECT is a buffer or string; nil means current buffer. ```elisp -(tp-match-reset PATTERN PLIST &optional OBJECT) -(tp-match-reset PATTERN LAYER-NAME &optional OBJECT) +(tp-match-reset PATTERN PLIST &optional OBJECT START END) +(tp-match-reset PATTERN LAYER-NAME &optional OBJECT START END) ``` +START and END restrict matching to the `[START, END)` portion of OBJECT +(see [`tp-match-set`](#tp-match-set---match-string)). + **Examples:** ```elisp @@ -1120,10 +1203,13 @@ LAYER-NAME can be a symbol representing a layer defined by `define-tp` or a grou OBJECT is a buffer or string; nil means current buffer. ```elisp -(tp-match-add PATTERN PLIST &optional OBJECT) -(tp-match-add PATTERN LAYER-NAME &optional OBJECT) +(tp-match-add PATTERN PLIST &optional OBJECT START END) +(tp-match-add PATTERN LAYER-NAME &optional OBJECT START END) ``` +START and END restrict matching to the `[START, END)` portion of OBJECT +(see [`tp-match-set`](#tp-match-set---match-string)). + **Examples:** ```elisp @@ -1155,8 +1241,8 @@ OBJECT is a buffer or string; nil means current buffer. #### `tp-regexp-set` - Match Regexp ```elisp -(tp-regexp-set PATTERN PLIST &optional OBJECT) -(tp-regexp-set PATTERN LAYER-NAME &optional OBJECT) +(tp-regexp-set PATTERN PLIST &optional OBJECT START END SUBEXP) +(tp-regexp-set PATTERN LAYER-NAME &optional OBJECT START END SUBEXP) ``` Set properties on all matches of a regular expression. @@ -1164,6 +1250,15 @@ PATTERN can be a string (single regexp) or a list of strings (multiple regexps). PLIST is a property list like `'(face bold help-echo "tip")`. LAYER-NAME can be a symbol representing a layer defined by `define-tp` or a group defined by `define-tps`. OBJECT is a buffer or string; nil means current buffer. +START and END (new in 0.3.0) restrict matching to the `[START, END)` portion +of OBJECT, in native coordinates; matching behaves as if OBJECT consisted +only of that portion, and reversed bounds are swapped +(see [`tp-match-set`](#tp-match-set---match-string)). +SUBEXP (new in 0.3.0) names a capture group of PATTERN (1 = first group, as +in font-lock highlights): properties apply to that group of each match +instead of the whole match. A match in which the group does not participate +contributes nothing; a SUBEXP beyond the pattern's group count signals a +clear error. All three `tp-regexp-*` functions accept SUBEXP. **Examples:** @@ -1192,6 +1287,28 @@ OBJECT is a buffer or string; nil means current buffer. (insert "abc 123 def 456") (tp-regexp-set "[0-9]+" 'number-style)) ;; => ((5 . 8) (13 . 16)) + +;; SUBEXP - propertize only capture group 1 of each match +(tp-regexp-set "\\([0-9]+\\)px" '(face bold) "margin: 10px 4px" nil nil 1) +;; => #("margin: 10px 4px" 8 10 (face bold) 13 14 (face bold)) + +;; A match whose group does not participate contributes nothing: +;; "bar" matches the pattern, but group 1 only participates in "foo" +(tp-regexp-set "\\(foo\\)\\|bar" '(face bold) "foo bar" nil nil 1) +;; => #("foo bar" 0 3 (face bold)) + +;; SUBEXP beyond the pattern's group count signals a clear error +(tp-regexp-set "[0-9]+" '(face bold) "abc 123" nil nil 2) +;; error: Regexp "[0-9]+" has no group 2 + +;; START/END bounds: as if only that portion existed - the greedy a+ +;; matches exactly [1, 3) instead of the whole run +(tp-regexp-set "a+" '(face bold) "aaaa" 1 3) +;; => #("aaaa" 1 3 (face bold)) + +;; Reversed bounds are swapped +(tp-regexp-set "a+" '(face bold) "aaaa" 3 1) +;; => #("aaaa" 1 3 (face bold)) ``` --- @@ -1205,10 +1322,13 @@ LAYER-NAME can be a symbol representing a layer defined by `define-tp` or a grou OBJECT is a buffer or string; nil means current buffer. ```elisp -(tp-regexp-reset PATTERN PLIST &optional OBJECT) -(tp-regexp-reset PATTERN LAYER-NAME &optional OBJECT) +(tp-regexp-reset PATTERN PLIST &optional OBJECT START END SUBEXP) +(tp-regexp-reset PATTERN LAYER-NAME &optional OBJECT START END SUBEXP) ``` +START/END bounds and the SUBEXP capture group work exactly as in +[`tp-regexp-set`](#tp-regexp-set---match-regexp). + **Examples:** ```elisp @@ -1247,10 +1367,13 @@ LAYER-NAME can be a symbol representing a layer defined by `define-tp` or a grou OBJECT is a buffer or string; nil means current buffer. ```elisp -(tp-regexp-add PATTERN PLIST &optional OBJECT) -(tp-regexp-add PATTERN LAYER-NAME &optional OBJECT) +(tp-regexp-add PATTERN PLIST &optional OBJECT START END SUBEXP) +(tp-regexp-add PATTERN LAYER-NAME &optional OBJECT START END SUBEXP) ``` +START/END bounds and the SUBEXP capture group work exactly as in +[`tp-regexp-set`](#tp-regexp-set---match-regexp). + **Examples:** ```elisp @@ -1284,21 +1407,28 @@ OBJECT is a buffer or string; nil means current buffer. #### `tp-search-forward` / `tp-search-backward` -```elisp -(tp-search-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) -(tp-search-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) -``` +> ⚠️ **Deprecated since 0.3.0.** These are raw wrappers for Emacs's +> `text-property-search-forward` / `text-property-search-backward` whose +> nil-PREDICATE default (match values that are non-nil and **not** `equal` +> to VALUE) contradicts the `equal`-matching used by the rest of the +> library. Use [`tp-forward` / `tp-backward`](#tp-forward--tp-backward) +> for tp's symmetric `equal`-matching search — they now expose PREDICATE +> and NOT-CURRENT too — or call the Emacs primitives directly for raw +> access. The wrappers keep working, but are marked obsolete (the byte +> compiler warns on new callers). -Raw wrappers for Emacs's `text-property-search-forward` and `text-property-search-backward`. -These are low-level search functions that work directly with prop-match objects. +```elisp +(tp-search-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) ; deprecated +(tp-search-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) ; deprecated +``` --- #### `tp-forward` / `tp-backward` ```elisp -(tp-forward PROPERTY &optional VALUE OBJECT N) -(tp-backward PROPERTY &optional VALUE OBJECT N) +(tp-forward PROPERTY &optional VALUE OBJECT N PREDICATE NOT-CURRENT) +(tp-backward PROPERTY &optional VALUE OBJECT N PREDICATE NOT-CURRENT) ``` Search forward/backward N times for text with PROPERTY. @@ -1310,10 +1440,16 @@ Search forward/backward N times for text with PROPERTY. - **`tp-backward` mirrors `tp-forward`**: the same equal-matching semantics, in the opposite direction. - **OBJECT** can be a buffer or string; nil defaults to current buffer. +- **PREDICATE** (new in 0.3.0) customizes matching: nil (the default) and t + both keep the 0.2.0 `equal`-matching contract **exactly**; a function is + called with `(VALUE PROP-VALUE)` and matches when it returns non-nil. +- **NOT-CURRENT** (new in 0.3.0), when non-nil, skips a matching region + containing point, mirroring the `text-property-search-*` primitives. + Buffer path only; strings have no point, so it is ignored there. - For buffers, returns the prop-match object from the last successful search. -- For strings, returns a list of (START END VALUE) for runs where PROPERTY - is present; VALUE nil means any value. `tp-backward` returns them from - end to start. +- For strings, returns a list of (START END VALUE) for the **first N** runs + where PROPERTY matches, counted from position 0 (point is not involved); + VALUE nil means any value. `tp-backward` returns them from end to start. **Examples:** @@ -1362,6 +1498,38 @@ Search forward/backward N times for text with PROPERTY. (tp-set 12 17 '(marker t) my-string) (tp-forward 'marker nil my-string 2)) ;; => ((0 5 t) (12 17 t)) + +;; PREDICATE - match with a custom function instead of `equal' +;; (called with VALUE and the region's property value) +(with-temp-buffer + (insert "abcdef") + (tp-set 1 3 '(size 10)) + (tp-set 3 6 '(size 20)) + (goto-char 1) + (let ((match (tp-forward 'size 15 nil 1 + (lambda (target v) (and v (> v target)))))) + (list (prop-match-beginning match) (prop-match-end match)))) +;; => (3 6) ; the first run whose size exceeds 15 + +;; PREDICATE works on strings too (returns the first N matching runs) +(let ((str (copy-sequence "hello world"))) + (tp-set 0 5 '(size 10) str) + (tp-set 6 11 '(size 20) str) + (tp-forward 'size 15 str 2 (lambda (target v) (and v (> v target))))) +;; => ((6 11 20)) + +;; NOT-CURRENT - skip the matching region containing point +(with-temp-buffer + (insert "one two") + (tp-set 1 4 '(mark t)) + (tp-set 5 8 '(mark t)) + (let (a b) + (goto-char 2) ; inside the first mark region + (setq a (prop-match-beginning (tp-forward 'mark t))) + (goto-char 2) + (setq b (prop-match-beginning (tp-forward 'mark t nil 1 nil t))) + (list a b))) +;; => (2 5) ; without NOT-CURRENT the current region matches at point ``` --- @@ -1369,11 +1537,15 @@ Search forward/backward N times for text with PROPERTY. #### `tp-forward-do` / `tp-backward-do` ```elisp -(tp-forward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END) -(tp-backward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END) +(tp-forward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END PREDICATE NOT-CURRENT) +(tp-backward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END PREDICATE NOT-CURRENT) ``` -Search forward/backward for text with PROPERTY and apply FUNCTION **only to the last match**. +Search forward/backward TIMES times for text with PROPERTY and apply FUNCTION **only at the TIMES-th match**. + +Despite the `-do` suffix this is **not** a for-each — use +[`tp-search-map`](#tp-search-map---apply-function-to-matched-text) to apply +a function to *every* match. - **FUNCTION** receives `(TEXT &optional START END IDX)` where TEXT is the matched text, START and END are the positions of the match, and IDX is the 0-based match index. FUNCTION is called with as many of these arguments as it accepts. When FUNCTION returns a string, it replaces the matched text in the string or buffer. - **Replacements may change length in buffers** (the match is deleted and the replacement inserted). **Strings cannot change length in place**: a replacement of a different length signals an error; same-length replacements are applied in place. @@ -1382,6 +1554,9 @@ Search forward/backward for text with PROPERTY and apply FUNCTION **only to the - **OBJECT** can be a buffer or string; nil defaults to current buffer. - **TIMES** is the number of searches, defaulting to 1. The function searches TIMES times but only applies FUNCTION to the TIMES-th match. All-or-nothing: if fewer than TIMES matches exist, FUNCTION is not applied at all and the number of available matches is returned. - **START** and **END** define the search range; defaults are object start and end. +- **PREDICATE** and **NOT-CURRENT** (new in 0.3.0) work as in + [`tp-forward` / `tp-backward`](#tp-forward--tp-backward) and are applied + to each underlying search; the defaults keep the 0.2.0 behavior exactly. - Returns the number of successful matches. **Examples:** @@ -1401,7 +1576,9 @@ Search forward/backward for text with PROPERTY and apply FUNCTION **only to the (tp-set 12 17 '(marker t) my-string) (tp-forward-do #'upcase 'marker nil my-string 2 6 17) my-string) -;; => "hello world HELLO" ; Only 1 match in range 6-17 +;; => "hello world hello" ; only 1 match in range 6-17, so the +;; requested 2nd match does not exist: nothing is applied +;; (all-or-nothing; the call still returns the count, 1) ;; Using function with start and end parameters ;; The function receives position info; use upcase to keep same length @@ -1641,9 +1818,16 @@ Text property layers is a **unique feature** of tp.el that requires specific fun #### `define-tp` / `define-tps` - Define Custom Text Properties +> Since 0.3.0 the prefix-conforming aliases `tp-define-layer` (for +> `define-tp`), `tp-define-group` (for `define-tps`) and +> `tp-define-palette` (for `define-tp-palette`) are the canonical names +> going forward — they make the macros discoverable via `C-h f tp-...`. +> The historical names are permanent aliases and will never be removed; +> this README's examples keep using them. + ##### `define-tp` - Define Single Custom Text Property (Layer) -Define a custom text property. The name does not need to be quoted. The ARGLIST is **mandatory in every format**: `()` for non-parameterized layers (including the reactive keyword format), `(ARG)` for parameterized layers. Supports three formats: +Define a custom text property. The name does not need to be quoted. The ARGLIST is **mandatory in every format**: `()` for non-parameterized layers (including the reactive keyword format), `(ARG1 ARG2 ...)` with any number of parameter symbols for parameterized layers. Supports three formats: **Format 1 - Non-parameterized (empty argument list, simple properties):** @@ -1656,7 +1840,7 @@ Define a custom text property. The name does not need to be quoted. The ARGLIST (tp-set 0 5 '(tp-bold t) "emacs") ``` -**Format 2 - Parameterized (with single argument):** +**Format 2 - Parameterized (with one or more arguments):** ```elisp (define-tp tp-space (pixel) @@ -1667,6 +1851,43 @@ Define a custom text property. The name does not need to be quoted. The ARGLIST (tp-set 0 5 '(tp-space 2) "emacs") ``` +Since 0.3.0 the arglist may declare **any number of parameters**. The call +specs accept the arguments flat — `(LAYER ARG1 ... ARGN)` — or wrapped in +one list — `(LAYER (ARG1 ... ARGN))` — and both work in `tp-set` and +`tp-put-layer`: + +```elisp +(define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + +;; Whole-string form: arguments follow the layer name +(tp-set "hello" 'tp-colors "red" "blue") +;; => #("hello" 0 5 (face (:foreground "red" :background "blue"))) + +;; Region form, wrapped argument list plus extra properties +(let ((str (copy-sequence "hello"))) + (tp-set 0 5 '(tp-colors ("red" "blue") help-echo "tip") str) + (list (tp-at 0 'face str) (tp-at 0 'help-echo str))) +;; => ((:foreground "red" :background "blue") "tip") + +;; tp-put-layer spec +(with-temp-buffer + (insert "Hello World") + (tp-put-layer 1 10 '(tp-colors "white" "black") 0) + (tp-at 1 'face)) +;; => (:foreground "white" :background "black") + +;; Wrong-arity calls signal a clear error naming the layer and both counts +(tp-set "hello" 'tp-colors "red") +;; error: tp layer tp-colors takes 2 argument(s), got 1 +``` + +Parameterized groups (`define-tps`) accept multiple parameters the same +way; the `(GROUP ARG1 ... ARGN)` and `(GROUP (ARG1 ... ARGN))` specs work +in the `tp-set` family. Note: `$`-symbols in parameterized bodies resolve +to their variables' current values at expansion time — parameterized +layers are **not** reactive. + **Format 3 - With reactive features (:props, :data, :compute, :watch, :transform):** ```elisp @@ -1697,7 +1918,7 @@ takes a function. ##### `define-tps` - Define Custom Text Property Group (Layer Group) -Define multiple related custom text properties. The name does not need to be quoted. As with `define-tp`, the ARGLIST is **mandatory**: `()` for non-parameterized groups, `(ARG)` for parameterized ones. Properties in the group can be used individually or with the group name to set multiple layers. +Define multiple related custom text properties. The name does not need to be quoted. As with `define-tp`, the ARGLIST is **mandatory**: `()` for non-parameterized groups, `(ARG1 ARG2 ...)` for parameterized ones (any number of parameters since 0.3.0). Properties in the group can be used individually or with the group name to set multiple layers. **Format 1 - Non-parameterized (empty argument list):** @@ -1876,6 +2097,91 @@ reactive engine uses it to locate and re-render their regions. --- +#### `tp-layer-props-with-args` / `tp-group-props-with-args` / `tp-layer-arglist` + +```elisp +(tp-layer-props-with-args LAYER-NAME ARGS &optional INCLUDE-TP-NAME) +(tp-group-props-with-args GROUP-NAME ARGS &optional INCLUDE-TP-NAME) +(tp-layer-arglist LAYER-NAME) +``` + +Introspection for **parameterized** layers and groups (new in 0.3.0): + +- **`tp-layer-props-with-args`** expands a parameterized layer with ARGS, + a list of values bound positionally to the layer's parameters. Extra + values are ignored; fewer values than parameters signal a wrong-arity + error. Returns a fresh copy (mutating it cannot corrupt the registry), + or nil for non-parameterized or undefined layers. The existing + single-argument `tp-layer-props-with-arg` (note the one-character name + difference) remains as a thin `(list ARG)` wrapper. +- **`tp-group-props-with-args`** is the group counterpart, returning the + list of expanded per-layer plists; `tp-group-props-with-arg` remains + as the single-argument convenience. +- **`tp-layer-arglist`** returns a copy of the layer's parameter list, + or nil when LAYER-NAME is not a parameterized layer. + +**Examples:** + +```elisp +(progn + (tp-layer-reset) + (define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + (tp-layer-props-with-args 'tp-colors '("red" "blue"))) +;; => (face (:foreground "red" :background "blue")) + +;; The parameter list itself +(tp-layer-arglist 'tp-colors) +;; => (fg bg) + +;; Groups expand to one plist per layer +(progn + (define-tps tp-badge (fg bg) + `(tp-colors ,fg ,bg) + '(face bold)) + (tp-group-props-with-args 'tp-badge '("white" "black"))) +;; => ((face (:foreground "white" :background "black")) (face bold)) + +;; Too few arguments signal the same clear arity error as tp-set +(tp-layer-props-with-args 'tp-colors '("red")) +;; error: tp layer tp-colors takes 2 argument(s), got 1 +``` + +--- + +#### `tp-describe-layer` - Describe a Layer + +```elisp +(tp-describe-layer NAME) ; interactive +``` + +Pop a help buffer describing layer NAME (with completion over all +registered layers when called interactively). The buffer shows the +storage format (flat / unified / parameterized / reactive), the raw +stored body, the expanded properties (or a placeholder for parameterized +layers, which need arguments), the parameter list, the reactive +variables the layer depends on, whether a transform is registered, and +the group that generated the layer, if any. + +```elisp +(progn + (tp-layer-reset) + (define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + (tp-describe-layer 'tp-colors)) +;; Pops a *Help* buffer: +;; tp-colors is a tp layer. +;; +;; Storage format: parameterized +;; Arguments: (fg bg) +;; Stored body: `(face (:foreground ,fg :background ,bg)) +;; Expanded props: parameterized layer: expand with `tp-layer-props-with-args' +;; Reactive deps: none +;; Transform: no +``` + +--- + #### `tp-undefine-layer` / `tp-undefine-group` ```elisp @@ -1958,14 +2264,30 @@ This is useful when you want to remove all reactive bindings but keep the layer ### Property Layer Placement +> ⚠️ **String forms of stack operations mutate in place.** Unlike `tp-set`, +> which returns a **new** propertized string, the string form of every stack +> mutator (`tp-put-layer`, `tp-push-layer`, `tp-pop-layer`, `tp-delete-layer`, +> `tp-move-layer`, `tp-raise-layer`, `tp-lower-layer`, `tp-rotate-layer`, +> `tp-pin-layer`, `tp-switch-layer`, `tp-hide-layer`, `tp-show-layer`, +> `tp-merge-layers`, `tp-flatten-layers`, `tp-add-to-layers`, +> `tp-add-to-all-layers`) modifies STRING **destructively**. Never pass a +> string literal or a shared string you do not own — use `copy-sequence` +> first. Unifying this with `tp-set`'s copy semantics is on the 0.4 ledger. + +**Return values (0.3.0):** `tp-put-layer` / `tp-push-layer` return OBJECT +when one was given (the string itself in string forms), else +`(START . END)`. Every other stack mutator returns the **number of property +runs it modified**; a missing layer name or index never signals — unmatched +runs are silently left alone, and a return value of 0 means nothing matched. + #### `tp-put-layer` - Set Layer at Index ```elisp ;; Buffer/string region -(tp-put-layer START END LAYER IDX OBJECT) +(tp-put-layer START END LAYER IDX OBJECT NOERROR) ;; Entire string -(tp-put-layer STRING LAYER IDX) +(tp-put-layer STRING LAYER IDX NOERROR) ``` Set layer(s) at a specific index position in the layer stack. @@ -1979,12 +2301,18 @@ LAYER accepts several specs: - a layer name defined with `define-tp`: `'highlight` - an inline property plist (no `define-tp` needed): `'(face bold help-echo "tip")` - a list of layer names (the first name ends up on top): `'(layer-a layer-b)` -- a parameterized layer call: `'(tp-color "red")` +- a parameterized layer call: `'(tp-color "red")` — multi-argument layers + work too: `'(tp-colors "white" "black")` **Stack model:** only the top layer's properties are the visible text properties; lower layers are stored in the `tp-layers` text property until they are raised, rotated, or flattened. +**NOERROR (new in 0.3.0):** a LAYER naming an undefined layer or group +normally signals an error. With NOERROR non-nil the call returns nil +instead and modifies nothing — handy when applying layers that may not be +defined yet. `tp-push-layer` accepts the same trailing NOERROR. + **Examples:** ```elisp @@ -2051,6 +2379,12 @@ they are raised, rotated, or flattened. (tp-put-layer 1 10 '(tp-color "red") 0) (tp-at 1 'face))) ;; => (:foreground "red") + +;; NOERROR - an undefined layer name returns nil instead of signaling +(with-temp-buffer + (insert "Hello World") + (tp-put-layer 1 10 'no-such-layer 0 nil t)) +;; => nil ; nothing modified ``` --- @@ -2059,13 +2393,16 @@ they are raised, rotated, or flattened. ```elisp ;; Buffer/string region -(tp-push-layer START END LAYER OBJECT) +(tp-push-layer START END LAYER OBJECT NOERROR) ;; Entire string -(tp-push-layer STRING LAYER) +(tp-push-layer STRING LAYER NOERROR) ``` Push a layer to the top of the stack (equivalent to `tp-put-layer ... 0`). +NOERROR (new in 0.3.0) works as in +[`tp-put-layer`](#tp-put-layer---set-layer-at-index): an undefined LAYER +returns nil instead of signaling. **Examples:** @@ -2317,17 +2654,72 @@ Raise a layer by N positions. Positive N moves toward top, negative moves toward --- -#### `tp-rotate-layer` - Cycle Layers +#### `tp-lower-layer` - Mirror of `tp-raise-layer` ```elisp ;; Buffer/string region -(tp-rotate-layer START END OBJECT) +(tp-lower-layer START END IDX/LAYER-NAME N OBJECT) ;; Entire string -(tp-rotate-layer STRING) +(tp-lower-layer STRING IDX/LAYER-NAME N) ``` -Rotate layers - top goes to bottom, next becomes visible. +Lower a layer by N positions (new in 0.3.0). The mirror image of +`tp-raise-layer`: positive N moves the layer down toward the bottom, +negative N moves it up. N defaults to 1, and the resulting position is +clamped to the stack. Returns the number of property runs modified. + +**Examples:** + +```elisp +;; Lower the top layer by one position +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + ;; Stack: layer3 (top), layer2, layer1 (bottom) + (tp-lower-layer 1 10 'layer3 1) + ;; Stack: layer2 (top), layer3, layer1 (bottom) + (list (tp-layer-top 1 10) (tp-layer-list 1 10)))) +;; => (layer2 (layer2 layer3 layer1)) +``` + +--- + +#### `tp-rotate-layer` - Cycle Layers + +```elisp +;; Buffer/string region (canonical order, OBJECT last - new in 0.3.0) +(tp-rotate-layer START END DIRECTION &optional COUNT OBJECT) + +;; Entire string +(tp-rotate-layer STRING DIRECTION COUNT) + +;; Buffer/string region (legacy order, kept working forever) +(tp-rotate-layer START END OBJECT) +``` + +Rotate layers by COUNT steps, preserving their relative order. + +- **DIRECTION** is `down` or nil to move the top layer to the bottom (the + historical behavior), or `up` to bring the bottom layer to the top; any + other value signals an error. +- **COUNT** is the number of rotation steps, defaulting to 1; a COUNT below + 1 rotates nothing. Hidden layers rotate with the rest of the stack. +- Returns the number of property runs modified. + +The two region orders are told apart by the third argument: the symbols +`up` / `down` are never valid OBJECTs, so `(tp-rotate-layer 1 5 'up)` +unambiguously selects the canonical `(START END DIRECTION [COUNT] +[OBJECT])` order — no nil OBJECT placeholder needed. Any other third +argument (a buffer, a string, or nil for the current buffer) selects the +legacy `(START END OBJECT [DIRECTION] [COUNT])` order, which keeps working. **Examples:** @@ -2346,6 +2738,37 @@ Rotate layers - top goes to bottom, next becomes visible. ;; Stack: base (top) -> highlight (bottom) (tp-layer-top 1 10))) ;; => base + +;; Canonical order: `up' brings the bottom layer to the top +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + ;; Stack: layer3 (top), layer2, layer1 (bottom) + (tp-rotate-layer 1 10 'up) + (tp-layer-list 1 10))) +;; => (layer1 layer3 layer2) + +;; COUNT rotates several steps at once +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + (tp-rotate-layer 1 10 'down 2) + (tp-layer-list 1 10))) +;; => (layer1 layer3 layer2) ``` --- @@ -2360,7 +2783,10 @@ Rotate layers - top goes to bottom, next becomes visible. (tp-pin-layer STRING IDX/LAYER-NAME) ``` -Move a specific layer to the top (make it visible). +Move a layer to the top of the stack. **One-shot**: despite the name, +nothing stays pinned — this is a single move to index 0, and nothing +prevents a later `tp-push-layer` or `tp-put-layer` from covering the moved +layer again. **Examples:** @@ -2415,6 +2841,111 @@ Swap positions of two layers. --- +### Property Layer Visibility + +#### `tp-hide-layer` / `tp-show-layer` - Hide and Show Layers + +```elisp +;; Buffer/string region +(tp-hide-layer START END NAME OBJECT) +(tp-show-layer START END NAME OBJECT) + +;; Entire string +(tp-hide-layer STRING NAME) +(tp-show-layer STRING NAME) +``` + +Hide a layer without removing it, and make it render again (new in 0.3.0). +NAME identifies the layer: a layer name symbol or an integer index into the +full stack, hidden layers included (0 = top, -1 = bottom). + +**The visibility model:** + +- A hidden layer **stays in the stack**: it still counts for + `tp-layer-count`, appears in `tp-layer-list` and `tp-layer-stack-at`, and + can be moved, raised, or lowered — but it does not render. The text shows + the properties of the topmost **non-hidden** layer instead. +- Hiding the currently visible top layer therefore reveals the next visible + layer below it. +- When **every** layer is hidden the text renders bare (only the + `tp-layers` bookkeeping property remains — not even `tp-name` renders) + while all layers stay queryable. +- A hidden layer **keeps receiving reactive updates** while hidden, so + `tp-show-layer` always reveals current values (see + [Layer-Buffer Registry & Lifecycle](#layer-buffer-registry--lifecycle)). +- `tp-flatten-layers` merges only visible layers, and `tp-merge-layers` + excludes hidden matched layers' properties — hiding can never leak (see + [Property Layer Merging](#property-layer-merging)). +- Hiddenness is stored as a `tp-hidden` flag inside the layer's plist in + `tp-layers` stack storage, so `tp-hidden` is a reserved property name + inside layers, like `tp-name`. + +Both functions return the number of property runs modified. A NAME matching +no layer never signals, and hiding an already-hidden layer (or showing a +visible one) is a silent no-op — 0 means nothing changed. + +**Examples:** + +```elisp +;; Hiding the top layer reveals the one below; the stack is intact +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (list :visible (tp-at 1 'tp-name) + :face (tp-at 1 'face) + :count (tp-layer-count 1 10) + :layers (tp-layer-list 1 10)))) +;; => (:visible base :face default :count 2 :layers (highlight base)) + +;; With every layer hidden the text renders bare +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-hide-layer 1 10 'base) + (list :face (tp-at 1 'face) :count (tp-layer-count 1 10)))) +;; => (:face nil :count 2) + +;; tp-show-layer restores the layer's rendering +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-show-layer 1 10 'highlight) + (tp-at 1 'face))) +;; => (:background "yellow") + +;; Return value: number of modified runs; a missing name is a silent 0 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (list (tp-hide-layer 1 10 'base) + (tp-hide-layer 1 10 'base) ; already hidden + (tp-hide-layer 1 10 'nonexistent)))) ; no such layer +;; => (1 0 0) +``` + +--- + ### Property Layer Merging #### `tp-merge-layers` - Merge Multiple Layers @@ -2429,6 +2960,14 @@ Swap positions of two layers. Merge specified layers into a new layer. Earlier layers in the list take precedence. +**Hidden layers (0.3.0):** hidden matched layers are merged away with the +rest but contribute **no** properties to the merged layer, so a merge can +never render what was hidden. When *every* matched layer is hidden, the +merged layer keeps their merged properties but carries the `tp-hidden` flag +itself — the data is preserved without un-hiding anything, and +`tp-show-layer` on the merged layer renders it. Returns the number of +property runs modified (0 = no listed layer matched). + **Examples:** ```elisp @@ -2457,6 +2996,22 @@ Merge specified layers into a new layer. Earlier layers in the list take precede (tp-merge-layers 1 10 'merged '(0 1)) (tp-layer-count 1 10))) ;; => 1 + +;; A hidden layer's properties never leak into the merge +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(help-echo "tip")) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-hide-layer 1 10 'layer2) + (tp-merge-layers 1 10 'merged '(layer1 layer2)) + (list :face (tp-at 1 'face) + :help (tp-at 1 'help-echo) + :name (tp-at 1 'tp-name)))) +;; => (:face bold :help nil :name merged) ; layer2 was hidden ``` --- @@ -2473,6 +3028,13 @@ Merge specified layers into a new layer. Earlier layers in the list take precede Flatten all layers into a single layer with the given name. +**Hidden layers (0.3.0):** hidden layers are **discarded**, mirroring +image-editor flatten semantics — only the visible layers' properties merge +into the result, so flattening can never render what was hidden. When +*every* layer of a run is hidden, the run's properties are cleared entirely +(bare text), consistent with the all-hidden rendering of `tp-hide-layer`. +Returns the number of property runs modified (0 = no run had layers). + **Examples:** ```elisp @@ -2499,6 +3061,20 @@ Flatten all layers into a single layer with the given name. (tp-flatten-layers 1 10 nil) (tp-at 1 'tp-name))) ;; => nil + +;; Hidden layers are discarded by flatten +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-flatten-layers 1 10 'flat) + (list (tp-at 1 'face) (tp-at 1 'tp-name)))) +;; => (default flat) ; highlight's background is gone ``` --- @@ -2585,7 +3161,10 @@ Check if layer exists in region. (tp-layer-top START END &optional OBJECT) ``` -Get name of the top (visible) layer. +Get name of the top layer. The topmost layer is reported in **stack +order**, even when it is hidden (see +[`tp-hide-layer`](#tp-hide-layer--tp-show-layer---hide-and-show-layers)); +use `tp-layer-stack-at` to distinguish hidden layers from visible ones. **Examples:** @@ -2604,6 +3183,63 @@ Get name of the top (visible) layer. --- +#### `tp-layer-stack-at` - Full Stack at a Position + +```elisp +(tp-layer-stack-at POS &optional OBJECT) +``` + +Return the full ordered layer stack at one position (new in 0.3.0), as a +list with one element per layer, topmost first, where each element is a +cons `(NAME . PROPS)`: + +- **NAME** is the layer's `tp-name` symbol, or nil for an unnamed layer. +- **PROPS** is the layer's property plist without its `tp-name` entry. A + hidden layer is distinguishable by a `tp-hidden` entry with value t in + PROPS; visible layers never carry one. + +Hidden layers are included at their stack position. Returns nil for bare +text. POS is in OBJECT's native coordinates (0-based for strings, 1-based +for buffers); OBJECT is a string, a buffer, or nil for the current buffer. + +**Examples:** + +```elisp +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-layer-stack-at 1))) +;; => ((highlight . (face (:background "yellow"))) +;; (base . (face default))) + +;; Hidden layers carry a `tp-hidden' entry in PROPS +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-layer-stack-at 1))) +;; => ((highlight . (tp-hidden t face (:background "yellow"))) +;; (base . (face default))) + +;; Bare text has no stack +(with-temp-buffer + (insert "Hello") + (tp-layer-stack-at 1)) +;; => nil +``` + +--- + #### `tp-add-to-layers` - Add Properties to Specific Layers ```elisp @@ -2619,7 +3255,9 @@ Add or merge properties to specific layers in a region or string. - **IDX-OR-LAYER-NAME-LIST** is a list of layer indices (integers) or layer names (symbols). For indices: 0 means top layer, -1 means bottom layer. - Properties are deeply merged into the specified layers (nested plists are merged, not replaced). - OBJECT defaults to current buffer for region form. -- For strings, returns a NEW string (original is not modified). For buffers, returns nil. +- Like the other stack mutators (and unlike `tp-set`), the string form + modifies STRING **in place** and returns that same mutated string. For + buffers, returns nil. **Examples:** @@ -2654,7 +3292,9 @@ Add or merge properties to all layers in a region or string. - Properties are deeply merged into all existing layers. - OBJECT defaults to current buffer for region form. -- For strings, returns a NEW string (original is not modified). For buffers, returns nil. +- Like the other stack mutators (and unlike `tp-set`), the string form + modifies STRING **in place** and returns that same mutated string. For + buffers, returns nil. **Examples:** @@ -2674,7 +3314,7 @@ Add or merge properties to all layers in a region or string. #### `tp-intervals` - Get Text Property Intervals ```elisp -(tp-intervals START END &optional OBJECT) +(tp-intervals START END &optional OBJECT ABSOLUTE) ``` Get all text property intervals from START to END in OBJECT. @@ -2682,8 +3322,12 @@ Get all text property intervals from START to END in OBJECT. - Returns a list of (START END PROPERTIES) for each interval, including gap intervals with no properties, whose PROPERTIES is nil. - For buffer input, START and END are 1-based buffer positions but the - returned positions are **0-based offsets relative to START**. For strings, - positions are absolute 0-based indices. + returned positions are by default **0-based offsets relative to START** + (the legacy convention). With ABSOLUTE non-nil (new in 0.3.0) they are + native 1-based buffer positions instead, directly reusable in other tp + calls (`tp-set`, `tp-remove`, ...) without offset arithmetic. For + strings, positions are always absolute 0-based indices; ABSOLUTE changes + nothing. - Uses `object-intervals` (requires Emacs 28.1+). - OBJECT can be a buffer or string; nil defaults to current buffer. @@ -2697,6 +3341,24 @@ Get all text property intervals from START to END in OBJECT. (tp-intervals 1 12)) ;; => ((0 5 (face bold)) (5 6 nil) (6 11 (face italic))) ;; positions are offsets from START; (5 6 nil) is the unpropertized gap + +;; ABSOLUTE - native buffer coordinates +(with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (tp-set 7 12 '(face italic)) + (tp-intervals 1 12 nil t)) +;; => ((1 6 (face bold)) (6 7 nil) (7 12 (face italic))) + +;; ABSOLUTE positions feed straight back into other tp calls +(with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (dolist (iv (tp-intervals 1 12 nil t)) + (when (eq (plist-get (nth 2 iv) 'face) 'bold) + (tp-add (nth 0 iv) (nth 1 iv) '(help-echo "bold text")))) + (tp-at 1 'help-echo)) +;; => "bold text" ``` --- @@ -2704,13 +3366,21 @@ Get all text property intervals from START to END in OBJECT. #### `tp-intervals-map` - Apply Function to Intervals ```elisp -(tp-intervals-map FUNCTION START END &optional OBJECT) +(tp-intervals-map FUNCTION START END &optional OBJECT ABSOLUTE) ``` Apply FUNCTION to all intervals between START and END in OBJECT. -- FUNCTION receives four arguments: interval-start, interval-end, top-props (visible layer properties), and below-props-lst (list of hidden layers). -- Intervals with no properties are visited too, with nil top-props (positions follow the same offset convention as `tp-intervals`). +- FUNCTION receives four arguments: interval-start, interval-end, + top-props (the directly rendered properties, with the `tp-layers` entry + removed), and below-props-lst (the `tp-layers` value: the stored layer + plists buried below the rendered top layer — while any layer is hidden + it holds the whole ordered stack; see + [`tp-layer-stack-at`](#tp-layer-stack-at---full-stack-at-a-position) for + the decoded view). +- Intervals with no properties are visited too, with nil top-props + (positions follow the same coordinate convention as `tp-intervals`, + including the ABSOLUTE argument, new in 0.3.0). - OBJECT can be a buffer or string; nil defaults to current buffer. - Returns list of function results (nil results are removed). @@ -2726,6 +3396,17 @@ Apply FUNCTION to all intervals between START and END in OBJECT. (list start end (plist-get props 'face))) 1 12)) ;; => ((0 5 bold) (5 6 nil) (6 11 italic)) + +;; ABSOLUTE - FUNCTION receives native buffer positions +(with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (tp-set 7 12 '(face italic)) + (tp-intervals-map + (lambda (start end props belows) + (list start end (plist-get props 'face))) + 1 12 nil t)) +;; => ((1 6 bold) (6 7 nil) (7 12 italic)) ``` --- @@ -2852,7 +3533,8 @@ parameterized `tp-palette` layer (as in `(tp-set "emacs" 'tp-palette 'info)`). - **`tp-palette-alist`** (variable) — alist of `(NAME . PLIST)` palette definitions; the single source of truth for palette lookups. Each PLIST maps `:fg`, `:bg`, and `:border` to colors. -- **`define-tp-palette`** — register (or update) a palette: +- **`define-tp-palette`** — register (or update) a palette (since 0.3.0 + also available as the prefix-conforming alias `tp-define-palette`): ```elisp (define-tp-palette my-brand @@ -2860,6 +3542,38 @@ parameterized `tp-palette` layer (as in `(tp-set "emacs" 'tp-palette 'info)`). :bg ("#ddf4ff" . "#1f3d5c")) ``` +- **`tp-palette-color`** (new in 0.3.0) — **the** palette accessor: get a + palette's `:fg` / `:bg` / `:border` color, resolved for the current + light/dark theme. Returns nil for a missing palette or key: + + ```elisp + (tp-palette-color 'info :fg) + ;; => "#0969da" on a light theme, "#58a6ff" on a dark theme + (tp-palette-color 'no-such-palette :fg) + ;; => nil + ``` + +- **`tp-palette-has-p`** (new in 0.3.0) — **the** palette predicate: with + just SYMBOL, test whether it names a registered palette; with KIND one of + `:fg` / `:bg` / `:border`, additionally require that key in its + definition (a defined key may still resolve to no color for the current + theme — use `tp-palette-color` when the resolved color matters): + + ```elisp + (list (tp-palette-has-p 'info) + (tp-palette-has-p 'info :border) + (tp-palette-has-p 'no-such-palette)) + ;; => (t t nil) + ``` + + The older per-key conveniences remain as compatible wrappers: + `tp-palette-fg-color` / `tp-palette-bg-color` / `tp-palette-border-color` + (fixed-KEY variants of `tp-palette-color`), `tp-palette-p` (nil-KIND + `tp-palette-has-p`), and the suffixed-name predicates `tp-palette-fg-p` / + `tp-palette-bg-p` / `tp-palette-fbg-p` / `tp-palette-border-p`, which + answer a different question: whether a *variant name* like `info-fg` + denotes a registered palette (the `tp-palette` layer's convention). + - **`tp-palette-show`** — interactive command that displays a gallery buffer of every registered palette and its `-fg` / `-bg` / `-fbg` / `-border` variants (`q` quits). @@ -3196,6 +3910,135 @@ Benefits of batched updates: - Improves performance when changing multiple variables - Ensures consistent state when multiple variables are interdependent +### Layer-Buffer Registry & Lifecycle + +Since 0.3.0 the reactive engine keeps a **layer→buffer registry**: every +buffer-mutating write path that stamps a layer (the `tp-set` family, the +stack mutators, the match/regexp appliers) registers the target buffer as +showing that layer, and a reactive update visits **only the registered +buffers** instead of scanning the whole `(buffer-list)`. Killed buffers are +pruned automatically. When a layer has no registry entry at all, one +**learning full scan** falls back to the old behavior and registers every +buffer where the layer is actually found. + +Updates reach a layer's regions even while the layer is **hidden** or +**buried** below other layers in a stack: the stored `tp-layers` entry is +updated in place, so `tp-show-layer` (or raising the layer) always reveals +current values. + +#### `tp-reactive-layer-buffers` - Inspect the Registry + +```elisp +(tp-reactive-layer-buffers LAYER-NAME) +``` + +Return the live buffers registered as showing LAYER-NAME — a list (possibly +empty, meaning "known: no buffer shows this layer") — or the symbol +`unknown` when the layer has no registry entry at all: + +```elisp +(progn + (tp-layer-reset) + (defvar reg-color "red") + (define-tp reg-layer () + :props '(face (:foreground $reg-color))) + (tp-reactive-layer-buffers 'reg-layer)) +;; => unknown ; never applied to any buffer yet + +(with-temp-buffer + (rename-buffer "demo-buffer" t) + (insert "Hello") + (tp-push-layer 1 6 'reg-layer) + (mapcar #'buffer-name (tp-reactive-layer-buffers 'reg-layer))) +;; => ("demo-buffer") +``` + +#### `tp-reactive-track-buffer` - Close the String-Insert Gap + +```elisp +(tp-reactive-track-buffer &optional BUFFER) ; interactive +``` + +**Known gap:** inserting an *already-propertized string* into a buffer +bypasses the buffer operations that register buffers, so that buffer is +missing from the registry until a learning full scan finds it. Call +`tp-reactive-track-buffer` after such an insert: it scans BUFFER (default: +the current buffer) for layer regions — rendered top layers as well as +layers buried or hidden inside `tp-layers` stack storage — registers the +buffer for each, and returns the layer names found in buffer order: + +```elisp +(let ((s (tp-set "hello" 'reg-layer))) ; propertized string, detached + (with-temp-buffer + (insert s) ; bypasses registration + (tp-reactive-track-buffer))) +;; => (reg-layer) ; buffer now registered for reg-layer +``` + +#### `tp-gc-anonymous-layers` - Collect Unused Anonymous Layers + +```elisp +(tp-gc-anonymous-layers) ; interactive +``` + +[Anonymous reactive layers](#anonymous-reactive-layers) are interned: an +`equal` props spec reuses its registry entry instead of minting a new layer +on every `tp-set`. `tp-gc-anonymous-layers` undefines every interned +anonymous layer that no registered live buffer still displays (buried and +hidden layers count as alive) and returns the collected layer names: + +```elisp +(defvar tmp-color "green") +(let ((buf (generate-new-buffer "*gc-demo*"))) + (with-current-buffer buf + (insert "Hello") + (tp-set 1 6 '(face (:foreground $tmp-color)))) ; anonymous layer + (kill-buffer buf) + (tp-gc-anonymous-layers)) +;; => (tp-anon-1) ; the collected names (the counter varies) +``` + +**Conservative `unknown` semantics:** a layer whose registry state is +`unknown` — never seen in any buffer through the registering paths, for +example referenced only by detached strings — is deliberately **kept**. A +layer becomes collectable only after it was registered for at least one +buffer and none of the registered buffers still shows it (e.g. all killed). +Call `tp-reactive-track-buffer` after inserting propertized strings so +their buffers are registered too. + +#### Minimal-Diff `tp-text` Re-Rendering + +Reactive `tp-text` replacements edit only the **differing span** of the old +and new text (inserting before deleting), so point and markers in unchanged +text keep their positions; point inside the edited span lands at the edit +start. An update to an **identical** value is a true no-op: no text edit, +no property churn, and the buffer-modified flag is untouched. + +```elisp +(progn + (tp-layer-reset) + (defvar counter-val "0") + (define-tp counter-label () + :props '(tp-text $counter-val)) + (with-temp-buffer + (insert "count: 0 items") + (tp-set 8 9 'counter-label) + (let ((m (copy-marker 10))) ; marker on the "i" of "items" + (setq counter-val "9") ; only the digit is edited + (list (buffer-substring-no-properties 1 (point-max)) + (char-after m))))) +;; => ("count: 9 items" ?i) ; the marker still points at its character + +;; Identical-value updates do not touch the buffer at all +(with-temp-buffer + (insert "count: 9 items") + (tp-set 8 9 'counter-label) + (set-buffer-modified-p nil) + (setq counter-val "9") ; same text as displayed + (buffer-modified-p)) +;; => nil +``` + ### Debug Mode tp.el provides a debug mode to help understand reactive update flow: diff --git a/README_CN.md b/README_CN.md index bae2aa5..0e0699e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -61,6 +61,8 @@ - [属性层定义](#属性层定义) - [define-tp / define-tps](#define-tp--define-tps---定义自定义文本属性) - [tp-layer-props / tp-group-props](#tp-layer-props--tp-group-props) + - [tp-layer-props-with-args / tp-group-props-with-args / tp-layer-arglist](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) + - [tp-describe-layer](#tp-describe-layer---描述属性层) - [tp-undefine-layer / tp-undefine-group](#tp-undefine-layer--tp-undefine-group) - [tp-layer-reset](#tp-layer-reset) - [tp-reactive-reset](#tp-reactive-reset) @@ -73,9 +75,12 @@ - [属性层移动](#属性层移动) - [tp-move-layer](#tp-move-layer---移动属性层到指定位置) - [tp-raise-layer](#tp-raise-layer---上移下移属性层) + - [tp-lower-layer](#tp-lower-layer---tp-raise-layer-的镜像) - [tp-rotate-layer](#tp-rotate-layer---轮换属性层) - [tp-pin-layer](#tp-pin-layer---将属性层置顶) - [tp-switch-layer](#tp-switch-layer---交换两个属性层) + - [属性层可见性](#属性层可见性) + - [tp-hide-layer / tp-show-layer](#tp-hide-layer--tp-show-layer---隐藏与显示属性层) - [属性层合并](#属性层合并) - [tp-merge-layers](#tp-merge-layers---合并多个属性层) - [tp-flatten-layers](#tp-flatten-layers---扁平化所有属性层) @@ -84,6 +89,7 @@ - [tp-layer-count](#tp-layer-count) - [tp-layer-exists-p](#tp-layer-exists-p) - [tp-layer-top](#tp-layer-top) + - [tp-layer-stack-at](#tp-layer-stack-at---获取某位置的完整层栈) - [tp-add-to-layers](#tp-add-to-layers---向特定属性层添加属性) - [tp-add-to-all-layers](#tp-add-to-all-layers---向所有属性层添加属性) - [实用工具函数](#实用工具函数) @@ -106,6 +112,7 @@ - [API 中的层名解析](#api-中的层名解析) - [响应式层组](#响应式层组) - [批量更新](#批量更新) + - [层-缓冲区注册表与生命周期](#层-缓冲区注册表与生命周期) - [调试模式](#调试模式) - [重置响应式状态](#重置响应式状态) - [完整示例:主题感知文本](#完整示例主题感知文本) @@ -184,6 +191,42 @@ ``` - ✅ **统一对象支持**:同一个函数同时支持字符串和缓冲区,无需记忆不同的 API +**只需记住一条规则**:当第一个参数是**字符串**时,调用作用于整个字符串; +当第一个参数是**数字**时,调用作用于 OBJECT 的 `[START, END)` 区域 —— 而 +OBJECT 总是位于最后(nil 表示当前缓冲区)。所有核心函数和层栈函数都遵循 +这条规则。 + +匹配/搜索家族(`tp-match-*`、`tp-regexp-*`、`tp-search-map`、 +`tp-forward-do`/`tp-backward-do`)刻意采用了**第二种约定**:PATTERN +(或 FUNCTION)和 PLIST 在前,然后是 OBJECT,最后才是可选的 START/END +边界。对这些函数来说,作用于整个对象才是常见用法,因此 OBJECT 位于范围 +参数之前而不是之后。 + +**返回值约定**(自 0.3.0 起): + +| 函数家族 | 返回值 | +|---|---| +| `tp-set` / `tp-reset` / `tp-add` | 缓冲区/区域形式返回 `(START . END)`;整字符串形式返回一个**新**字符串 | +| `tp-remove` | 缓冲区形式返回 nil;整字符串形式返回一个**新**字符串 | +| `tp-clear` | nil | +| `tp-match-*` / `tp-regexp-*` | 缓冲区返回 `(START . END)` 匹配列表;字符串返回一个**新**字符串 | +| 栈修改函数(delete/pop/move/raise/lower/rotate/pin/switch/hide/show/merge/flatten) | 被修改的属性区段数量(0 = 没有匹配任何层) | +| `tp-put-layer` / `tp-push-layer` | 给定 OBJECT 时返回 OBJECT(字符串形式返回该字符串本身),否则返回 `(START . END)` | +| `tp-add-to-layers` / `tp-add-to-all-layers` | 字符串形式返回该字符串本身(**就地**修改);缓冲区返回 nil | + +**命名空间地图**:接受*层名*参数的 `tp-layer-NAME` 函数 +(`tp-layer-props`、`tp-layer-arglist` 等)查询的是层**注册表**(层定 +义);接受*位置*参数的函数 —— START END(`tp-layer-list`、 +`tp-layer-count`、`tp-layer-top` 等)或单个 POS(`tp-layer-stack-at`) +—— 查询的是实际文本上的层**栈**。 + +**命名约定**:`tp-define-layer` / `tp-define-group` / +`tp-define-palette` 是今后符合前缀规范的规范名称(可通过 +`C-h f tp-...` 发现);`define-tp` / `define-tps` / `define-tp-group` / +`define-tp-palette` 是永久别名,永远不会被移除(本 README 的示例仍使用 +历史名称)。`tp-search-forward` / `tp-search-backward` 自 0.3.0 起已废 +弃 —— 参见[搜索和导航函数](#tp-search-forward--tp-search-backward)。 + ### 三种属性操作语义 原生 API 只有简单的设置和获取,tp.el 提供了三种清晰的操作语义: @@ -254,9 +297,10 @@ - ✅ **丰富的属性层操作**: - 放置:`tp-put-layer`(指定位置)、`tp-push-layer`(顶部) - 删除:`tp-delete-layer`(按名称/索引)、`tp-pop-layer`(顶层) - - 移动:`tp-raise-layer`(上下移动)、`tp-rotate-layer`(轮换)、`tp-pin-layer`(置顶)、`tp-switch-layer`(交换) + - 移动:`tp-raise-layer` / `tp-lower-layer`(上下移动)、`tp-rotate-layer`(轮换)、`tp-pin-layer`(一次性置顶)、`tp-switch-layer`(交换) + - 可见性:`tp-hide-layer` / `tp-show-layer`(隐藏属性层而不移除它) - 合并:`tp-merge-layers`(合并指定层)、`tp-flatten-layers`(扁平化所有层) -- ✅ **属性层查询**:`tp-layer-list`、`tp-layer-count`、`tp-layer-exists-p`、`tp-layer-top` +- ✅ **属性层查询**:`tp-layer-list`、`tp-layer-count`、`tp-layer-exists-p`、`tp-layer-top`、`tp-layer-stack-at` ```elisp ;; 属性层使用示例 @@ -299,6 +343,7 @@ - ✅ **:data 附加状态**:定义不直接用于属性但可以触发更新的额外响应式变量 - ✅ **:compute 计算属性**:创建从其他响应式变量派生值的计算属性(类似 Vue 的 computed) - ✅ **:watch 副作用监听**:当响应式变量改变时执行回调函数(类似 Vue 的 watch) +- ✅ **定向更新(0.3.0)**:层→缓冲区注册表使更新只访问展示受影响层的缓冲区;`tp-text` 重渲染只编辑差异区段(point 和标记保持原位);`tp-reactive-track-buffer` / `tp-gc-anonymous-layers` 管理层的生命周期 —— 参见[层-缓冲区注册表与生命周期](#层-缓冲区注册表与生命周期) ```elisp ;; 定义一个带响应式属性的层 @@ -327,8 +372,8 @@ ### 增强的搜索与导航 - ✅ **范围搜索**:`tp-search` 返回所有匹配区间的列表 -- ✅ **N次搜索**:`tp-forward`/`tp-backward` 支持向前/向后搜索N次 -- ✅ **搜索并执行**:`tp-forward-do`/`tp-backward-do` 搜索并对匹配文本执行函数 +- ✅ **N次搜索**:`tp-forward`/`tp-backward` 支持向前/向后搜索N次,并支持可选的 PREDICATE 匹配和 NOT-CURRENT +- ✅ **搜索并执行**:`tp-forward-do`/`tp-backward-do` 搜索 N 次并在第 N 个匹配处应用函数 - ✅ **批量转换**:`tp-search-map` 对所有匹配应用转换函数 ```elisp @@ -403,32 +448,37 @@ tp.el 所有函数按类别组织的完整概览: #### 模式匹配函数 | 函数 | 描述 | |------|------| -| [`tp-match-set`](#tp-match-set---匹配字符串) | 在字符串匹配处设置属性 | -| [`tp-match-reset`](#tp-match-reset---匹配并重置) | 在字符串匹配处重置所有属性 | -| [`tp-match-add`](#tp-match-add---匹配并添加) | 在字符串匹配处添加/合并属性 | -| [`tp-regexp-set`](#tp-regexp-set---匹配正则表达式) | 在正则匹配处设置属性 | -| [`tp-regexp-reset`](#tp-regexp-reset---正则匹配并重置) | 在正则匹配处重置所有属性 | -| [`tp-regexp-add`](#tp-regexp-add---正则匹配并添加) | 在正则匹配处添加/合并属性 | +| [`tp-match-set`](#tp-match-set---匹配字符串) | 在字符串匹配处设置属性(可选边界) | +| [`tp-match-reset`](#tp-match-reset---匹配并重置) | 在字符串匹配处重置所有属性(可选边界) | +| [`tp-match-add`](#tp-match-add---匹配并添加) | 在字符串匹配处添加/合并属性(可选边界) | +| [`tp-regexp-set`](#tp-regexp-set---匹配正则表达式) | 在正则匹配处设置属性(可选边界和捕获组) | +| [`tp-regexp-reset`](#tp-regexp-reset---正则匹配并重置) | 在正则匹配处重置所有属性(可选边界和捕获组) | +| [`tp-regexp-add`](#tp-regexp-add---正则匹配并添加) | 在正则匹配处添加/合并属性(可选边界和捕获组) | #### 搜索和导航函数 | 函数 | 描述 | |------|------| -| [`tp-search-forward`](#tp-search-forward--tp-search-backward) | text-property-search-forward 的原始包装 | -| [`tp-search-backward`](#tp-search-forward--tp-search-backward) | text-property-search-backward 的原始包装 | -| [`tp-forward`](#tp-forward--tp-backward) | 向前搜索 N 次具有属性的文本(支持缓冲区和字符串) | -| [`tp-backward`](#tp-forward--tp-backward) | 向后搜索 N 次具有属性的文本(支持缓冲区和字符串) | -| [`tp-forward-do`](#tp-forward-do--tp-backward-do) | 向前搜索并对最后一个匹配应用函数(支持起始和结束范围) | -| [`tp-backward-do`](#tp-forward-do--tp-backward-do) | 向后搜索并对最后一个匹配应用函数(支持起始和结束范围) | +| [`tp-search-forward`](#tp-search-forward--tp-search-backward) | **已废弃(0.3.0)** —— 请使用 [`tp-forward`](#tp-forward--tp-backward) 或 Emacs 原语 | +| [`tp-search-backward`](#tp-search-forward--tp-search-backward) | **已废弃(0.3.0)** —— 请使用 [`tp-backward`](#tp-forward--tp-backward) 或 Emacs 原语 | +| [`tp-forward`](#tp-forward--tp-backward) | 向前搜索 N 次具有属性的文本(可选谓词匹配) | +| [`tp-backward`](#tp-forward--tp-backward) | 向后搜索 N 次具有属性的文本(可选谓词匹配) | +| [`tp-forward-do`](#tp-forward-do--tp-backward-do) | 向前搜索 N 次,在第 N 个匹配处应用函数 | +| [`tp-backward-do`](#tp-forward-do--tp-backward-do) | 向后搜索 N 次,在第 N 个匹配处应用函数 | | [`tp-search`](#tp-search---搜索所有匹配) | 在范围或字符串中搜索所有匹配的属性 | | [`tp-search-map`](#tp-search-map---对匹配文本应用函数) | 对所有匹配的文本应用函数(支持起始和结束范围) | #### 属性层定义函数 | 函数 | 描述 | |------|------| -| [`define-tp`](#define-tp--define-tps---定义自定义文本属性) | 定义自定义文本属性(层),支持参数化 | -| [`define-tps`](#define-tp--define-tps---定义自定义文本属性) | 定义自定义文本属性组(层组),支持参数化 | +| [`define-tp`](#define-tp--define-tps---定义自定义文本属性) | 定义自定义文本属性(层),支持可选参数 | +| [`define-tps`](#define-tp--define-tps---定义自定义文本属性) | 定义自定义文本属性组(层组),支持可选参数 | +| [`tp-define-layer` / `tp-define-group`](#define-tp--define-tps---定义自定义文本属性) | `define-tp` / `define-tps` 的前缀规范别名 | | [`tp-layer-props`](#tp-layer-props--tp-group-props) | 获取属性层的属性 | | [`tp-group-props`](#tp-layer-props--tp-group-props) | 获取属性层组中所有属性层的属性 | +| [`tp-layer-props-with-args`](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) | 用参数列表展开参数化属性层 | +| [`tp-group-props-with-args`](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) | 用参数列表展开参数化属性层组 | +| [`tp-layer-arglist`](#tp-layer-props-with-args--tp-group-props-with-args--tp-layer-arglist) | 获取参数化属性层的参数列表 | +| [`tp-describe-layer`](#tp-describe-layer---描述属性层) | 在帮助缓冲区中描述属性层的定义 | | [`tp-undefine-layer`](#tp-undefine-layer--tp-undefine-group) | 移除属性层定义 | | [`tp-undefine-group`](#tp-undefine-layer--tp-undefine-group) | 移除属性层组定义 | | [`tp-layer-reset`](#tp-layer-reset) | 清除所有属性层/属性层组定义 | @@ -437,8 +487,8 @@ tp.el 所有函数按类别组织的完整概览: #### 属性层放置函数 | 函数 | 描述 | |------|------| -| [`tp-put-layer`](#tp-put-layer---在指定位置设置属性层) | 在指定索引位置设置属性层 | -| [`tp-push-layer`](#tp-push-layer---推送属性层到顶部) | 将属性层推到堆栈顶部 | +| [`tp-put-layer`](#tp-put-layer---在指定位置设置属性层) | 在指定索引位置设置属性层(可选 NOERROR) | +| [`tp-push-layer`](#tp-push-layer---推送属性层到顶部) | 将属性层推到堆栈顶部(可选 NOERROR) | #### 属性层删除函数 | 函数 | 描述 | @@ -451,15 +501,22 @@ tp.el 所有函数按类别组织的完整概览: |------|------| | [`tp-move-layer`](#tp-move-layer---移动属性层到指定位置) | 将属性层从一个位置移动到另一个位置 | | [`tp-raise-layer`](#tp-raise-layer---上移下移属性层) | 将属性层上移/下移 N 个位置 | -| [`tp-rotate-layer`](#tp-rotate-layer---轮换属性层) | 轮换属性层(顶层移到底部) | -| [`tp-pin-layer`](#tp-pin-layer---将属性层置顶) | 将属性层置顶(使其可见) | +| [`tp-lower-layer`](#tp-lower-layer---tp-raise-layer-的镜像) | `tp-raise-layer` 的镜像:将属性层下移/上移 N 个位置 | +| [`tp-rotate-layer`](#tp-rotate-layer---轮换属性层) | 向上或向下轮换属性层 N 步 | +| [`tp-pin-layer`](#tp-pin-layer---将属性层置顶) | 将属性层移到顶部(一次性;之后的 push 仍可能覆盖它) | | [`tp-switch-layer`](#tp-switch-layer---交换两个属性层) | 交换两个属性层的位置 | +#### 属性层可见性函数 +| 函数 | 描述 | +|------|------| +| [`tp-hide-layer`](#tp-hide-layer--tp-show-layer---隐藏与显示属性层) | 隐藏属性层而不将其从栈中移除 | +| [`tp-show-layer`](#tp-hide-layer--tp-show-layer---隐藏与显示属性层) | 让隐藏的属性层重新渲染 | + #### 属性层合并函数 | 函数 | 描述 | |------|------| -| [`tp-merge-layers`](#tp-merge-layers---合并多个属性层) | 将指定属性层合并为新属性层 | -| [`tp-flatten-layers`](#tp-flatten-layers---扁平化所有属性层) | 将所有属性层扁平化为单一属性层 | +| [`tp-merge-layers`](#tp-merge-layers---合并多个属性层) | 将指定属性层合并为新属性层(隐藏层不贡献属性) | +| [`tp-flatten-layers`](#tp-flatten-layers---扁平化所有属性层) | 将所有属性层扁平化为单一属性层(隐藏层被丢弃) | #### 属性层查询函数 | 函数 | 描述 | @@ -467,7 +524,8 @@ tp.el 所有函数按类别组织的完整概览: | [`tp-layer-list`](#tp-layer-list---列出所有属性层) | 列出区域中的所有属性层名称 | | [`tp-layer-count`](#tp-layer-count) | 计算区域中的属性层数量 | | [`tp-layer-exists-p`](#tp-layer-exists-p) | 检查区域中是否存在某属性层 | -| [`tp-layer-top`](#tp-layer-top) | 获取顶层(可见)属性层的名称 | +| [`tp-layer-top`](#tp-layer-top) | 获取顶层属性层的名称(按栈序,即使它被隐藏) | +| [`tp-layer-stack-at`](#tp-layer-stack-at---获取某位置的完整层栈) | 以 `(NAME . PROPS)` cons 形式返回某位置的完整有序层栈 | | [`tp-region-layer-props`](#tp-region-layer-props---获取区域中的层属性) | 获取区域中特定层的属性 | #### 属性层操作函数 @@ -479,8 +537,8 @@ tp.el 所有函数按类别组织的完整概览: #### 实用工具函数 | 函数 | 描述 | |------|------| -| [`tp-intervals`](#tp-intervals---获取文本属性区间) | 获取区域中的所有文本属性区间 | -| [`tp-intervals-map`](#tp-intervals-map---对区间应用函数) | 对区域中的所有区间应用函数 | +| [`tp-intervals`](#tp-intervals---获取文本属性区间) | 获取区域中的所有文本属性区间(可选 ABSOLUTE 坐标) | +| [`tp-intervals-map`](#tp-intervals-map---对区间应用函数) | 对区域中的所有区间应用函数(可选 ABSOLUTE 坐标) | | [`tp-plist`](#tp-plist---获取区域中的所有属性) | 获取区域中存在的所有属性 | | [`tp-empty-p`](#tp-empty-p---检查对象是否有属性) | 检查对象是否没有文本属性 | | [`tp-with-current-buffer`](#tp-with-current-buffer--tp-pop-to-buffer--tp-switch-to-buffer) | 在绑定 `inhibit-read-only` 的情况下在缓冲区中执行 body | @@ -491,10 +549,20 @@ tp.el 所有函数按类别组织的完整概览: | 函数 | 描述 | |------|------| | [`tp-palette-alist`](#调色板系统) | 具名调色板注册表(变量) | -| [`define-tp-palette`](#调色板系统) | 注册或更新一个具名调色板 | +| [`define-tp-palette`](#调色板系统) | 注册或更新一个具名调色板(别名:`tp-define-palette`) | +| [`tp-palette-color`](#调色板系统) | 获取调色板的 `:fg` / `:bg` / `:border` 颜色,按主题解析 | +| [`tp-palette-has-p`](#调色板系统) | 测试调色板(或其某个键)是否已定义 | | [`tp-palette-show`](#调色板系统) | 展示所有已注册调色板的画廊 | | [`tp-parse-color`](#调色板系统) | 按当前亮色/暗色主题解析颜色规格 | +#### 响应式生命周期函数 +| 函数 | 描述 | +|------|------| +| [`tp-with-batch-updates`](#批量更新) | 将多个响应式变量更改合并为一次更新 | +| [`tp-reactive-layer-buffers`](#层-缓冲区注册表与生命周期) | 注册为展示某层的缓冲区(或 `unknown`) | +| [`tp-reactive-track-buffer`](#层-缓冲区注册表与生命周期) | 插入已带属性的字符串后注册缓冲区 | +| [`tp-gc-anonymous-layers`](#层-缓冲区注册表与生命周期) | 回收已无注册的存活缓冲区展示的匿名层 | + --- ### 核心属性函数 @@ -982,7 +1050,7 @@ LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或 (tp-clear &optional START END OBJECT) ``` -清除区域中的所有文本属性。 +清除区域中的所有文本属性。返回 nil。 **示例:** @@ -1011,8 +1079,8 @@ LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或 #### `tp-match-set` - 匹配字符串 ```elisp -(tp-match-set PATTERN PLIST &optional OBJECT) -(tp-match-set PATTERN LAYER-NAME &optional OBJECT) +(tp-match-set PATTERN PLIST &optional OBJECT START END) +(tp-match-set PATTERN LAYER-NAME &optional OBJECT START END) ``` 在所有字符串模式匹配处设置属性。 @@ -1020,6 +1088,10 @@ PATTERN 可以是字符串(单个模式)或字符串列表(多个模式) PLIST 是属性列表,如 `'(face bold help-echo "tip")`。 LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或通过 `define-tps` 定义的属性组名称。 OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 +START 和 END(0.3.0 新增)将匹配限制在 OBJECT 的 `[START, END)` 部分, +使用原生坐标(字符串从 0 开始,缓冲区从 1 开始)。匹配的行为**如同 +OBJECT 只由这一部分组成**,因此匹配不会跨越边界;颠倒的边界会被交换。 +全部六个 `tp-match-*` / `tp-regexp-*` 函数都接受同样的边界参数。 **示例:** @@ -1052,6 +1124,12 @@ OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 (insert "TODO: fix this. TODO: also this.") (tp-match-set "TODO" 'todo-style)) ;; => ((1 . 5) (17 . 21)) + +;; 用 START/END 边界限制匹配 - 只有第二个 TODO 在范围内 +(with-temp-buffer + (insert "TODO one TODO two") + (tp-match-set "TODO" '(face warning) nil 5 18)) +;; => ((10 . 14)) ``` --- @@ -1065,10 +1143,13 @@ LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或 OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 ```elisp -(tp-match-reset PATTERN PLIST &optional OBJECT) -(tp-match-reset PATTERN LAYER-NAME &optional OBJECT) +(tp-match-reset PATTERN PLIST &optional OBJECT START END) +(tp-match-reset PATTERN LAYER-NAME &optional OBJECT START END) ``` +START 和 END 将匹配限制在 OBJECT 的 `[START, END)` 部分 +(参见 [`tp-match-set`](#tp-match-set---匹配字符串))。 + **示例:** ```elisp @@ -1106,10 +1187,13 @@ LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或 OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 ```elisp -(tp-match-add PATTERN PLIST &optional OBJECT) -(tp-match-add PATTERN LAYER-NAME &optional OBJECT) +(tp-match-add PATTERN PLIST &optional OBJECT START END) +(tp-match-add PATTERN LAYER-NAME &optional OBJECT START END) ``` +START 和 END 将匹配限制在 OBJECT 的 `[START, END)` 部分 +(参见 [`tp-match-set`](#tp-match-set---匹配字符串))。 + **示例:** ```elisp @@ -1141,8 +1225,8 @@ OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 #### `tp-regexp-set` - 匹配正则表达式 ```elisp -(tp-regexp-set PATTERN PLIST &optional OBJECT) -(tp-regexp-set PATTERN LAYER-NAME &optional OBJECT) +(tp-regexp-set PATTERN PLIST &optional OBJECT START END SUBEXP) +(tp-regexp-set PATTERN LAYER-NAME &optional OBJECT START END SUBEXP) ``` 在所有正则表达式匹配处设置属性。 @@ -1150,6 +1234,13 @@ PATTERN 可以是字符串(单个正则)或字符串列表(多个正则) PLIST 是属性列表,如 `'(face bold help-echo "tip")`。 LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或通过 `define-tps` 定义的属性组名称。 OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 +START 和 END(0.3.0 新增)将匹配限制在 OBJECT 的 `[START, END)` 部分, +使用原生坐标;匹配的行为如同 OBJECT 只由这一部分组成,颠倒的边界会被 +交换(参见 [`tp-match-set`](#tp-match-set---匹配字符串))。 +SUBEXP(0.3.0 新增)指定 PATTERN 的一个捕获组(1 = 第一个组,与 +font-lock 高亮的约定一致):属性应用于每个匹配中的该捕获组,而不是整个 +匹配。捕获组未参与的匹配不贡献任何内容;SUBEXP 超出模式的捕获组数量时 +会发出明确的错误信号。全部三个 `tp-regexp-*` 函数都接受 SUBEXP。 **示例:** @@ -1178,6 +1269,28 @@ OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 (insert "abc 123 def 456") (tp-regexp-set "[0-9]+" 'number-style)) ;; => ((5 . 8) (13 . 16)) + +;; SUBEXP - 只对每个匹配的捕获组 1 设置属性 +(tp-regexp-set "\\([0-9]+\\)px" '(face bold) "margin: 10px 4px" nil nil 1) +;; => #("margin: 10px 4px" 8 10 (face bold) 13 14 (face bold)) + +;; 捕获组未参与的匹配不贡献任何内容: +;; "bar" 匹配该模式,但捕获组 1 只在 "foo" 中参与 +(tp-regexp-set "\\(foo\\)\\|bar" '(face bold) "foo bar" nil nil 1) +;; => #("foo bar" 0 3 (face bold)) + +;; SUBEXP 超出模式的捕获组数量时发出明确的错误信号 +(tp-regexp-set "[0-9]+" '(face bold) "abc 123" nil nil 2) +;; error: Regexp "[0-9]+" has no group 2 + +;; START/END 边界:如同只有这一部分存在 - 贪婪的 a+ +;; 恰好匹配 [1, 3) 而不是整段字符 +(tp-regexp-set "a+" '(face bold) "aaaa" 1 3) +;; => #("aaaa" 1 3 (face bold)) + +;; 颠倒的边界会被交换 +(tp-regexp-set "a+" '(face bold) "aaaa" 3 1) +;; => #("aaaa" 1 3 (face bold)) ``` --- @@ -1191,10 +1304,13 @@ LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或 OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 ```elisp -(tp-regexp-reset PATTERN PLIST &optional OBJECT) -(tp-regexp-reset PATTERN LAYER-NAME &optional OBJECT) +(tp-regexp-reset PATTERN PLIST &optional OBJECT START END SUBEXP) +(tp-regexp-reset PATTERN LAYER-NAME &optional OBJECT START END SUBEXP) ``` +START/END 边界和 SUBEXP 捕获组的用法与 +[`tp-regexp-set`](#tp-regexp-set---匹配正则表达式) 完全相同。 + **示例:** ```elisp @@ -1233,10 +1349,13 @@ LAYER-NAME 可以是通过 `define-tp` 定义的自定义文本属性名称或 OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 ```elisp -(tp-regexp-add PATTERN PLIST &optional OBJECT) -(tp-regexp-add PATTERN LAYER-NAME &optional OBJECT) +(tp-regexp-add PATTERN PLIST &optional OBJECT START END SUBEXP) +(tp-regexp-add PATTERN LAYER-NAME &optional OBJECT START END SUBEXP) ``` +START/END 边界和 SUBEXP 捕获组的用法与 +[`tp-regexp-set`](#tp-regexp-set---匹配正则表达式) 完全相同。 + **示例:** ```elisp @@ -1270,21 +1389,27 @@ OBJECT 是缓冲区或字符串;nil 表示当前缓冲区。 #### `tp-search-forward` / `tp-search-backward` -```elisp -(tp-search-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) -(tp-search-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) -``` +> ⚠️ **自 0.3.0 起已废弃。**这两个函数是 Emacs 的 +> `text-property-search-forward` / `text-property-search-backward` 的原 +> 始包装,其 nil-PREDICATE 默认行为(匹配非 nil 且与 VALUE **不** +> `equal` 的值)与本库其余部分使用的 `equal` 匹配相矛盾。请使用 +> [`tp-forward` / `tp-backward`](#tp-forward--tp-backward) 获得 tp 对称 +> 的 `equal` 匹配搜索 —— 它们现在也暴露了 PREDICATE 和 NOT-CURRENT —— +> 或者直接调用 Emacs 原语进行底层访问。这两个包装仍然可用,但已被标记 +> 为过时(字节编译器会对新的调用发出警告)。 -Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的原始包装。 -这些是直接使用 prop-match 对象的底层搜索函数。 +```elisp +(tp-search-forward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) ; deprecated +(tp-search-backward PROPERTY &optional VALUE PREDICATE NOT-CURRENT) ; deprecated +``` --- #### `tp-forward` / `tp-backward` ```elisp -(tp-forward PROPERTY &optional VALUE OBJECT N) -(tp-backward PROPERTY &optional VALUE OBJECT N) +(tp-forward PROPERTY &optional VALUE OBJECT N PREDICATE NOT-CURRENT) +(tp-backward PROPERTY &optional VALUE OBJECT N PREDICATE NOT-CURRENT) ``` 向前/向后搜索 N 次具有 PROPERTY 的文本。 @@ -1296,9 +1421,16 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 - **`tp-backward` 与 `tp-forward` 对称**:相同的 equal 匹配语义, 方向相反。 - **OBJECT** 可以是缓冲区或字符串;nil 默认为当前缓冲区。 +- **PREDICATE**(0.3.0 新增)自定义匹配方式:nil(默认值)和 t 都 + **完全**保持 0.2.0 的 `equal` 匹配契约;传入函数时以 `(VALUE PROP-VALUE)` + 调用,返回非 nil 即视为匹配。 +- **NOT-CURRENT**(0.3.0 新增)非 nil 时跳过包含 point 的匹配区段,与 + `text-property-search-*` 原语的行为一致。仅缓冲区路径有效;字符串没 + 有 point,因此在字符串上会被忽略。 - 对于缓冲区,返回最后一次成功搜索的 prop-match 对象。 -- 对于字符串,返回 PROPERTY 存在的各区段的 (START END VALUE) 列表; - VALUE 为 nil 表示匹配任意值。`tp-backward` 按从末尾到开头的顺序返回。 +- 对于字符串,返回**前 N 个** PROPERTY 匹配区段的 (START END VALUE) + 列表,从位置 0 开始计数(与 point 无关);VALUE 为 nil 表示匹配任意 + 值。`tp-backward` 按从末尾到开头的顺序返回。 **示例:** @@ -1347,6 +1479,38 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 (tp-set 12 17 '(marker t) my-string) (tp-forward 'marker nil my-string 2)) ;; => ((0 5 t) (12 17 t)) + +;; PREDICATE - 用自定义函数代替 `equal' 进行匹配 +;; (调用参数为 VALUE 和该区段的属性值) +(with-temp-buffer + (insert "abcdef") + (tp-set 1 3 '(size 10)) + (tp-set 3 6 '(size 20)) + (goto-char 1) + (let ((match (tp-forward 'size 15 nil 1 + (lambda (target v) (and v (> v target)))))) + (list (prop-match-beginning match) (prop-match-end match)))) +;; => (3 6) ; 第一个 size 超过 15 的区段 + +;; PREDICATE 也适用于字符串(返回前 N 个匹配区段) +(let ((str (copy-sequence "hello world"))) + (tp-set 0 5 '(size 10) str) + (tp-set 6 11 '(size 20) str) + (tp-forward 'size 15 str 2 (lambda (target v) (and v (> v target))))) +;; => ((6 11 20)) + +;; NOT-CURRENT - 跳过包含 point 的匹配区段 +(with-temp-buffer + (insert "one two") + (tp-set 1 4 '(mark t)) + (tp-set 5 8 '(mark t)) + (let (a b) + (goto-char 2) ; 位于第一个 mark 区段内 + (setq a (prop-match-beginning (tp-forward 'mark t))) + (goto-char 2) + (setq b (prop-match-beginning (tp-forward 'mark t nil 1 nil t))) + (list a b))) +;; => (2 5) ; 不带 NOT-CURRENT 时当前区段在 point 处即匹配 ``` --- @@ -1354,11 +1518,14 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 #### `tp-forward-do` / `tp-backward-do` ```elisp -(tp-forward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END) -(tp-backward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END) +(tp-forward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END PREDICATE NOT-CURRENT) +(tp-backward-do FUNCTION PROPERTY &optional VALUE OBJECT TIMES START END PREDICATE NOT-CURRENT) ``` -在 OBJECT 的 START 到 END 范围内,向前/向后搜索匹配 PROPERTY 属性(值为 VALUE)的部分,**仅对最后一次匹配执行 FUNCTION 函数**。 +向前/向后搜索 TIMES 次具有 PROPERTY 的文本,**仅在第 TIMES 个匹配处应用 FUNCTION**。 + +尽管带有 `-do` 后缀,它并**不是** for-each —— 要对*每个*匹配应用函数, +请使用 [`tp-search-map`](#tp-search-map---对匹配文本应用函数)。 - **FUNCTION** 的参数是 `(TEXT &optional START END IDX)`,其中 TEXT 是此次匹配到的文本,START 和 END 为开始结束的位置,IDX 是从 0 开始的匹配索引。FUNCTION 会按其实际接受的参数个数被调用。当 FUNCTION 返回字符串时,它将替换字符串或缓冲区中的匹配文本。 - **在缓冲区中替换文本可以改变长度**(先删除匹配文本,再插入替换文本)。**字符串无法就地改变长度**:长度不同的替换会发出错误信号;长度相同的替换会就地应用。 @@ -1367,6 +1534,9 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 - **OBJECT** 默认是当前 buffer 或指定的字符串或指定的 buffer。 - **TIMES** 表示向前/向后搜索几次,默认搜索一次。该函数会搜索 TIMES 次,但仅对第 TIMES 个匹配应用 FUNCTION。要么全有要么全无:当匹配数量不足 TIMES 时,完全不应用 FUNCTION,仅返回实际找到的匹配数量。 - **START** 和 **END** 默认为 OBJECT 的起始和结束位置。 +- **PREDICATE** 和 **NOT-CURRENT**(0.3.0 新增)的用法与 + [`tp-forward` / `tp-backward`](#tp-forward--tp-backward) 相同,并被应 + 用到每一次底层搜索;默认值完全保持 0.2.0 的行为。 - 返回成功匹配的数量。 **示例:** @@ -1386,7 +1556,9 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 (tp-set 12 17 '(marker t) my-string) (tp-forward-do #'upcase 'marker nil my-string 2 6 17) my-string) -;; => "hello world HELLO" ; 范围 6-17 内仅有 1 个匹配 +;; => "hello world hello" ; 范围 6-17 内仅有 1 个匹配,请求的 +;; 第 2 个匹配不存在:不做任何应用(要么全有要么全无; +;; 调用仍返回实际匹配数 1) ;; 使用带有 start 和 end 参数的函数 ;; 函数接收位置信息;使用 upcase 保持相同长度 @@ -1624,9 +1796,15 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 #### `define-tp` / `define-tps` - 定义自定义文本属性 +> 自 0.3.0 起,符合前缀规范的别名 `tp-define-layer`(对应 +> `define-tp`)、`tp-define-group`(对应 `define-tps`)和 +> `tp-define-palette`(对应 `define-tp-palette`)是今后的规范名称 —— +> 它们让这些宏可以通过 `C-h f tp-...` 被发现。历史名称是永久别名,永远 +> 不会被移除;本 README 的示例仍继续使用它们。 + ##### `define-tp` - 定义单个自定义文本属性(层) -定义自定义文本属性,名称无需单引号引用。**所有格式中参数列表都是必需的**:无参数层(包括响应式关键字格式)用 `()`,参数化层用 `(ARG)`。支持三种格式: +定义自定义文本属性,名称无需单引号引用。**所有格式中参数列表都是必需的**:无参数层(包括响应式关键字格式)用 `()`,参数化层用 `(ARG1 ARG2 ...)`,可包含任意数量的参数符号。支持三种格式: **格式一 - 无参数(空参数列表,简单属性):** @@ -1639,7 +1817,7 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 (tp-set 0 5 '(tp-bold t) "emacs") ``` -**格式二 - 有参数(带单个参数):** +**格式二 - 有参数(带一个或多个参数):** ```elisp (define-tp tp-space (pixel) @@ -1650,6 +1828,42 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 (tp-set 0 5 '(tp-space 2) "emacs") ``` +自 0.3.0 起,参数列表可以声明**任意数量的参数**。调用规格既接受平铺的 +参数 —— `(LAYER ARG1 ... ARGN)` —— 也接受包在一个列表中的参数 —— +`(LAYER (ARG1 ... ARGN))` —— 两种写法在 `tp-set` 和 `tp-put-layer` 中 +都有效: + +```elisp +(define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + +;; 整字符串形式:参数跟在层名后面 +(tp-set "hello" 'tp-colors "red" "blue") +;; => #("hello" 0 5 (face (:foreground "red" :background "blue"))) + +;; 区域形式,包装的参数列表外加额外属性 +(let ((str (copy-sequence "hello"))) + (tp-set 0 5 '(tp-colors ("red" "blue") help-echo "tip") str) + (list (tp-at 0 'face str) (tp-at 0 'help-echo str))) +;; => ((:foreground "red" :background "blue") "tip") + +;; tp-put-layer 规格 +(with-temp-buffer + (insert "Hello World") + (tp-put-layer 1 10 '(tp-colors "white" "black") 0) + (tp-at 1 'face)) +;; => (:foreground "white" :background "black") + +;; 参数个数不符的调用会发出明确的错误信号,指出层名和两个数量 +(tp-set "hello" 'tp-colors "red") +;; error: tp layer tp-colors takes 2 argument(s), got 1 +``` + +参数化层组(`define-tps`)以同样的方式接受多个参数; +`(GROUP ARG1 ... ARGN)` 和 `(GROUP (ARG1 ... ARGN))` 规格在 `tp-set` +家族中都有效。注意:参数化 body 中的 `$` 符号在展开时解析为其变量的当 +前值 —— 参数化层**不是**响应式的。 + **格式三 - 响应式特性(支持 :props、:data、:compute、:watch、:transform):** ```elisp @@ -1679,7 +1893,7 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 ##### `define-tps` - 定义自定义文本属性组(层组) -定义多个相关的自定义文本属性,名称无需单引号引用。与 `define-tp` 一样,**参数列表是必需的**:无参数层组用 `()`,参数化层组用 `(ARG)`。属性组中定义的文本属性可以单独使用,也可以使用组名称来设置多层。 +定义多个相关的自定义文本属性,名称无需单引号引用。与 `define-tp` 一样,**参数列表是必需的**:无参数层组用 `()`,参数化层组用 `(ARG1 ARG2 ...)`(自 0.3.0 起支持任意数量的参数)。属性组中定义的文本属性可以单独使用,也可以使用组名称来设置多层。 **格式一 - 无参数(空参数列表):** @@ -1862,6 +2076,87 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 --- +#### `tp-layer-props-with-args` / `tp-group-props-with-args` / `tp-layer-arglist` + +```elisp +(tp-layer-props-with-args LAYER-NAME ARGS &optional INCLUDE-TP-NAME) +(tp-group-props-with-args GROUP-NAME ARGS &optional INCLUDE-TP-NAME) +(tp-layer-arglist LAYER-NAME) +``` + +针对**参数化**属性层和属性层组的自省函数(0.3.0 新增): + +- **`tp-layer-props-with-args`** 用 ARGS 展开参数化属性层,ARGS 是按位 + 置绑定到层参数的值列表。多余的值会被忽略;值的数量少于参数数量时会发 + 出参数个数不符的错误信号。返回一个全新的副本(修改它不会破坏注册 + 表);对无参数或未定义的层返回 nil。原有的单参数 + `tp-layer-props-with-arg`(注意名称只差一个字符)保留为一个薄薄的 + `(list ARG)` 包装。 +- **`tp-group-props-with-args`** 是层组版本,返回展开后的逐层 plist 列 + 表;`tp-group-props-with-arg` 保留为单参数便捷形式。 +- **`tp-layer-arglist`** 返回层参数列表的副本;当 LAYER-NAME 不是参数 + 化层时返回 nil。 + +**示例:** + +```elisp +(progn + (tp-layer-reset) + (define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + (tp-layer-props-with-args 'tp-colors '("red" "blue"))) +;; => (face (:foreground "red" :background "blue")) + +;; 参数列表本身 +(tp-layer-arglist 'tp-colors) +;; => (fg bg) + +;; 层组展开为每层一个 plist +(progn + (define-tps tp-badge (fg bg) + `(tp-colors ,fg ,bg) + '(face bold)) + (tp-group-props-with-args 'tp-badge '("white" "black"))) +;; => ((face (:foreground "white" :background "black")) (face bold)) + +;; 参数太少时发出与 tp-set 相同的明确错误信号 +(tp-layer-props-with-args 'tp-colors '("red")) +;; error: tp layer tp-colors takes 2 argument(s), got 1 +``` + +--- + +#### `tp-describe-layer` - 描述属性层 + +```elisp +(tp-describe-layer NAME) ; interactive +``` + +弹出一个帮助缓冲区,描述属性层 NAME(交互式调用时可在所有已注册层中补 +全)。该缓冲区会展示存储格式(flat / unified / parameterized / +reactive)、原始存储的 body、展开后的属性(参数化层需要参数,因此显示 +占位说明)、参数列表、该层依赖的响应式变量、是否注册了 transform,以及 +生成该层的层组(如果有)。 + +```elisp +(progn + (tp-layer-reset) + (define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + (tp-describe-layer 'tp-colors)) +;; 弹出一个 *Help* 缓冲区: +;; tp-colors is a tp layer. +;; +;; Storage format: parameterized +;; Arguments: (fg bg) +;; Stored body: `(face (:foreground ,fg :background ,bg)) +;; Expanded props: parameterized layer: expand with `tp-layer-props-with-args' +;; Reactive deps: none +;; Transform: no +``` + +--- + #### `tp-undefine-layer` / `tp-undefine-group` ```elisp @@ -1945,14 +2240,29 @@ Emacs 的 `text-property-search-forward` 和 `text-property-search-backward` 的 ### 属性层放置 +> ⚠️ **栈操作的字符串形式会就地修改字符串。**与返回**新**属性字符串的 +> `tp-set` 不同,每一个栈修改函数(`tp-put-layer`、`tp-push-layer`、 +> `tp-pop-layer`、`tp-delete-layer`、`tp-move-layer`、`tp-raise-layer`、 +> `tp-lower-layer`、`tp-rotate-layer`、`tp-pin-layer`、 +> `tp-switch-layer`、`tp-hide-layer`、`tp-show-layer`、 +> `tp-merge-layers`、`tp-flatten-layers`、`tp-add-to-layers`、 +> `tp-add-to-all-layers`)的字符串形式都会**破坏性地**修改 STRING。绝 +> 不要传入字符串字面量或不属于你的共享字符串 —— 请先用 +> `copy-sequence`。将这一行为与 `tp-set` 的复制语义统一已列入 0.4 计划。 + +**返回值(0.3.0):**`tp-put-layer` / `tp-push-layer` 在给定 OBJECT 时 +返回 OBJECT(字符串形式返回该字符串本身),否则返回 `(START . END)`。 +其余每个栈修改函数都返回**被修改的属性区段数量**;层名或索引不存在时 +从不发出错误信号 —— 未匹配的区段被静默跳过,返回 0 表示没有任何匹配。 + #### `tp-put-layer` - 在指定位置设置属性层 ```elisp ;; 缓冲区/字符串区域 -(tp-put-layer START END LAYER IDX OBJECT) +(tp-put-layer START END LAYER IDX OBJECT NOERROR) ;; 整个字符串 -(tp-put-layer STRING LAYER IDX) +(tp-put-layer STRING LAYER IDX NOERROR) ``` 在属性层堆栈的指定索引位置设置属性层。 @@ -1966,11 +2276,17 @@ LAYER 接受以下几种形式: - 用 `define-tp` 定义的层名:`'highlight` - 内联属性 plist(无需 `define-tp`):`'(face bold help-echo "tip")` - 层名列表(第一个层名位于顶部):`'(layer-a layer-b)` -- 参数化层调用:`'(tp-color "red")` +- 参数化层调用:`'(tp-color "red")` —— 多参数层同样可用: + `'(tp-colors "white" "black")` **栈模型:**只有顶层的属性是可见的文本属性;下层被保存在 `tp-layers` 文本属性中,直到被上移、轮换或扁平化。 +**NOERROR(0.3.0 新增):**LAYER 指向未定义的层或层组时,通常会发出错 +误信号。NOERROR 非 nil 时,调用改为返回 nil 且不做任何修改 —— 在应用 +可能尚未定义的层时非常方便。`tp-push-layer` 接受同样的末尾 NOERROR 参 +数。 + **示例:** ```elisp @@ -2037,6 +2353,12 @@ LAYER 接受以下几种形式: (tp-put-layer 1 10 '(tp-color "red") 0) (tp-at 1 'face))) ;; => (:foreground "red") + +;; NOERROR - 未定义的层名返回 nil 而不发出错误信号 +(with-temp-buffer + (insert "Hello World") + (tp-put-layer 1 10 'no-such-layer 0 nil t)) +;; => nil ; 没有任何修改 ``` --- @@ -2045,13 +2367,16 @@ LAYER 接受以下几种形式: ```elisp ;; 缓冲区/字符串区域 -(tp-push-layer START END LAYER OBJECT) +(tp-push-layer START END LAYER OBJECT NOERROR) ;; 整个字符串 -(tp-push-layer STRING LAYER) +(tp-push-layer STRING LAYER NOERROR) ``` 将属性层推到堆栈顶部(相当于 `tp-put-layer ... 0`)。 +NOERROR(0.3.0 新增)的用法与 +[`tp-put-layer`](#tp-put-layer---在指定位置设置属性层) 相同:未定义的 +LAYER 返回 nil 而不发出错误信号。 **示例:** @@ -2303,17 +2628,69 @@ LAYER 接受以下几种形式: --- -#### `tp-rotate-layer` - 轮换属性层 +#### `tp-lower-layer` - `tp-raise-layer` 的镜像 ```elisp ;; 缓冲区/字符串区域 -(tp-rotate-layer START END OBJECT) +(tp-lower-layer START END IDX/LAYER-NAME N OBJECT) ;; 整个字符串 -(tp-rotate-layer STRING) +(tp-lower-layer STRING IDX/LAYER-NAME N) ``` -轮换属性层 - 顶层移到底部,下一层变为可见。 +将属性层下移 N 个位置(0.3.0 新增)。它是 `tp-raise-layer` 的镜像:正 +数 N 向底部移动,负数 N 向顶部移动。N 默认为 1,最终位置会被钳制在栈的 +范围内。返回被修改的属性区段数量。 + +**示例:** + +```elisp +;; 将顶层下移一个位置 +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + ;; 堆栈: layer3 (顶), layer2, layer1 (底) + (tp-lower-layer 1 10 'layer3 1) + ;; 堆栈: layer2 (顶), layer3, layer1 (底) + (list (tp-layer-top 1 10) (tp-layer-list 1 10)))) +;; => (layer2 (layer2 layer3 layer1)) +``` + +--- + +#### `tp-rotate-layer` - 轮换属性层 + +```elisp +;; 缓冲区/字符串区域(规范顺序,OBJECT 在最后 - 0.3.0 新增) +(tp-rotate-layer START END DIRECTION &optional COUNT OBJECT) + +;; 整个字符串 +(tp-rotate-layer STRING DIRECTION COUNT) + +;; 缓冲区/字符串区域(历史顺序,永远保持可用) +(tp-rotate-layer START END OBJECT) +``` + +将属性层轮换 COUNT 步,保持它们的相对顺序。 + +- **DIRECTION** 为 `down` 或 nil 时将顶层移到底部(历史行为),为 + `up` 时将底层带到顶部;其他值会发出错误信号。 +- **COUNT** 是轮换的步数,默认为 1;COUNT 小于 1 时不做任何轮换。隐藏 + 层随栈中其他层一起轮换。 +- 返回被修改的属性区段数量。 + +两种区域顺序通过第三个参数区分:符号 `up` / `down` 永远不是合法的 +OBJECT,因此 `(tp-rotate-layer 1 5 'up)` 会无歧义地选中规范的 +`(START END DIRECTION [COUNT] [OBJECT])` 顺序 —— 无需 nil OBJECT 占位。 +第三个参数为其他值(缓冲区、字符串,或表示当前缓冲区的 nil)时选中历 +史的 `(START END OBJECT [DIRECTION] [COUNT])` 顺序,后者继续可用。 **示例:** @@ -2332,6 +2709,37 @@ LAYER 接受以下几种形式: ;; 堆栈: base (顶) -> highlight (底) (tp-layer-top 1 10))) ;; => base + +;; 规范顺序:`up' 将底层带到顶部 +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + ;; 堆栈: layer3 (顶), layer2, layer1 (底) + (tp-rotate-layer 1 10 'up) + (tp-layer-list 1 10))) +;; => (layer1 layer3 layer2) + +;; COUNT 一次轮换多步 +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + (tp-rotate-layer 1 10 'down 2) + (tp-layer-list 1 10))) +;; => (layer1 layer3 layer2) ``` --- @@ -2346,7 +2754,9 @@ LAYER 接受以下几种形式: (tp-pin-layer STRING IDX/LAYER-NAME) ``` -将特定属性层移到顶部(使其可见)。 +将属性层移到栈顶。**一次性操作**:尽管名字里有 pin,但没有任何东西会 +保持"钉住"状态 —— 这只是一次移动到索引 0 的操作,之后的 +`tp-push-layer` 或 `tp-put-layer` 仍然可以覆盖被移动的层。 **示例:** @@ -2401,6 +2811,105 @@ LAYER 接受以下几种形式: --- +### 属性层可见性 + +#### `tp-hide-layer` / `tp-show-layer` - 隐藏与显示属性层 + +```elisp +;; 缓冲区/字符串区域 +(tp-hide-layer START END NAME OBJECT) +(tp-show-layer START END NAME OBJECT) + +;; 整个字符串 +(tp-hide-layer STRING NAME) +(tp-show-layer STRING NAME) +``` + +隐藏属性层而不移除它,以及让它重新渲染(0.3.0 新增)。NAME 标识属性 +层:层名符号,或指向完整栈(包含隐藏层)的整数索引(0 = 顶层,-1 = +底层)。 + +**可见性模型:** + +- 隐藏层**仍留在栈中**:它仍计入 `tp-layer-count`,出现在 + `tp-layer-list` 和 `tp-layer-stack-at` 中,也可以被移动、上移或下移 + —— 但它不渲染。文本改为展示最顶部**未隐藏**层的属性。 +- 因此隐藏当前可见的顶层会显露它下面的下一个可见层。 +- 当**所有**层都被隐藏时,文本以裸文本渲染(只剩 `tp-layers` 这个簿记 + 属性 —— 连 `tp-name` 也不渲染),同时所有层仍然可查询。 +- 隐藏层在隐藏期间**持续接收响应式更新**,因此 `tp-show-layer` 总是显 + 露最新的值(参见[层-缓冲区注册表与生命周期](#层-缓冲区注册表与生命周期))。 +- `tp-flatten-layers` 只合并可见层,`tp-merge-layers` 排除隐藏的匹配层 + 的属性 —— 隐藏的内容绝不会泄漏(参见[属性层合并](#属性层合并))。 +- 隐藏状态以 `tp-hidden` 标志的形式存储在 `tp-layers` 栈存储内该层的 + plist 中,因此 `tp-hidden` 与 `tp-name` 一样是层内部的保留属性名。 + +两个函数都返回被修改的属性区段数量。NAME 不匹配任何层时从不发出错误信 +号,隐藏一个已隐藏的层(或显示一个可见的层)是静默的空操作 —— 返回 0 +表示没有任何变化。 + +**示例:** + +```elisp +;; 隐藏顶层会显露下面的层;栈保持完整 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (list :visible (tp-at 1 'tp-name) + :face (tp-at 1 'face) + :count (tp-layer-count 1 10) + :layers (tp-layer-list 1 10)))) +;; => (:visible base :face default :count 2 :layers (highlight base)) + +;; 所有层都隐藏时文本以裸文本渲染 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-hide-layer 1 10 'base) + (list :face (tp-at 1 'face) :count (tp-layer-count 1 10)))) +;; => (:face nil :count 2) + +;; tp-show-layer 恢复该层的渲染 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-show-layer 1 10 'highlight) + (tp-at 1 'face))) +;; => (:background "yellow") + +;; 返回值:被修改的区段数量;名称不存在时静默返回 0 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (list (tp-hide-layer 1 10 'base) + (tp-hide-layer 1 10 'base) ; 已经隐藏 + (tp-hide-layer 1 10 'nonexistent)))) ; 没有这个层 +;; => (1 0 0) +``` + +--- + ### 属性层合并 #### `tp-merge-layers` - 合并多个属性层 @@ -2415,6 +2924,12 @@ LAYER 接受以下几种形式: 将指定的属性层合并为一个新属性层。列表中靠前的属性层优先级更高。 +**隐藏层(0.3.0):**隐藏的匹配层会和其他层一起被合并掉,但**不**向合 +并层贡献任何属性,因此合并绝不会渲染出被隐藏的内容。当*所有*匹配层都 +被隐藏时,合并层保留它们合并后的属性,但自身携带 `tp-hidden` 标志 —— +数据被保留而没有取消任何隐藏,对合并层执行 `tp-show-layer` 即可渲染 +它。返回被修改的属性区段数量(0 = 列出的层都没有匹配)。 + **示例:** ```elisp @@ -2443,6 +2958,22 @@ LAYER 接受以下几种形式: (tp-merge-layers 1 10 'merged '(0 1)) (tp-layer-count 1 10))) ;; => 1 + +;; 隐藏层的属性绝不会泄漏进合并结果 +(progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(help-echo "tip")) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-hide-layer 1 10 'layer2) + (tp-merge-layers 1 10 'merged '(layer1 layer2)) + (list :face (tp-at 1 'face) + :help (tp-at 1 'help-echo) + :name (tp-at 1 'tp-name)))) +;; => (:face bold :help nil :name merged) ; layer2 被隐藏了 ``` --- @@ -2459,6 +2990,12 @@ LAYER 接受以下几种形式: 将所有属性层扁平化为一个具有给定名称的单一属性层。 +**隐藏层(0.3.0):**隐藏层会被**丢弃**,与图像编辑器的扁平化语义一致 +—— 只有可见层的属性会合并进结果,因此扁平化绝不会渲染出被隐藏的内容。 +当某个区段的*所有*层都被隐藏时,该区段的属性会被完全清除(裸文本), +与 `tp-hide-layer` 的全隐藏渲染行为一致。返回被修改的属性区段数量 +(0 = 没有区段带有属性层)。 + **示例:** ```elisp @@ -2485,6 +3022,20 @@ LAYER 接受以下几种形式: (tp-flatten-layers 1 10 nil) (tp-at 1 'tp-name))) ;; => nil + +;; 扁平化会丢弃隐藏层 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-flatten-layers 1 10 'flat) + (list (tp-at 1 'face) (tp-at 1 'tp-name)))) +;; => (default flat) ; highlight 的背景色消失了 ``` --- @@ -2571,7 +3122,9 @@ LAYER 接受以下几种形式: (tp-layer-top START END &optional OBJECT) ``` -获取顶层(可见)属性层的名称。 +获取顶层属性层的名称。最顶部的层按**栈序**报告,即使它被隐藏(参见 +[`tp-hide-layer`](#tp-hide-layer--tp-show-layer---隐藏与显示属性层)); +要区分隐藏层和可见层,请使用 `tp-layer-stack-at`。 **示例:** @@ -2590,6 +3143,61 @@ LAYER 接受以下几种形式: --- +#### `tp-layer-stack-at` - 获取某位置的完整层栈 + +```elisp +(tp-layer-stack-at POS &optional OBJECT) +``` + +返回某一位置上完整的有序层栈(0.3.0 新增),列表中每层一个元素,最顶 +层在前,每个元素是一个 cons `(NAME . PROPS)`: + +- **NAME** 是层的 `tp-name` 符号,无名层为 nil。 +- **PROPS** 是层的属性 plist,其中不含 `tp-name` 条目。隐藏层可通过 + PROPS 中值为 t 的 `tp-hidden` 条目辨认;可见层永远不携带该条目。 + +隐藏层按其栈位置包含在内。裸文本返回 nil。POS 使用 OBJECT 的原生坐标 +(字符串从 0 开始,缓冲区从 1 开始);OBJECT 是字符串、缓冲区,或表示 +当前缓冲区的 nil。 + +**示例:** + +```elisp +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-layer-stack-at 1))) +;; => ((highlight . (face (:background "yellow"))) +;; (base . (face default))) + +;; 隐藏层在 PROPS 中携带 `tp-hidden' 条目 +(progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-layer-stack-at 1))) +;; => ((highlight . (tp-hidden t face (:background "yellow"))) +;; (base . (face default))) + +;; 裸文本没有层栈 +(with-temp-buffer + (insert "Hello") + (tp-layer-stack-at 1)) +;; => nil +``` + +--- + #### `tp-add-to-layers` - 向特定属性层添加属性 ```elisp @@ -2605,7 +3213,8 @@ LAYER 接受以下几种形式: - **IDX-OR-LAYER-NAME-LIST** 是层索引(整数)或层名称(符号)的列表。对于索引:0 表示顶层,-1 表示底层。 - 属性被深度合并到指定的层中(嵌套的 plist 被合并,而非替换)。 - OBJECT 在区域形式中默认为当前缓冲区。 -- 对于字符串,返回一个新的字符串(原始字符串不变)。对于缓冲区,返回 nil。 +- 与其他栈修改函数一样(且与 `tp-set` 不同),字符串形式会**就地**修 + 改 STRING 并返回这个被修改的字符串本身。对于缓冲区,返回 nil。 **示例:** @@ -2640,7 +3249,8 @@ LAYER 接受以下几种形式: - 属性被深度合并到所有现有层中。 - OBJECT 在区域形式中默认为当前缓冲区。 -- 对于字符串,返回一个新的字符串(原始字符串不变)。对于缓冲区,返回 nil。 +- 与其他栈修改函数一样(且与 `tp-set` 不同),字符串形式会**就地**修 + 改 STRING 并返回这个被修改的字符串本身。对于缓冲区,返回 nil。 **示例:** @@ -2660,15 +3270,18 @@ LAYER 接受以下几种形式: #### `tp-intervals` - 获取文本属性区间 ```elisp -(tp-intervals START END &optional OBJECT) +(tp-intervals START END &optional OBJECT ABSOLUTE) ``` 从 OBJECT 中获取 START 到 END 之间的所有文本属性区间。 - 返回每个区间的 (START END PROPERTIES) 列表,包括没有属性的 间隙区间,其 PROPERTIES 为 nil。 -- 对于缓冲区输入,START 和 END 是从 1 开始的缓冲区位置,但返回的位置是 - **相对于 START 的 0 基偏移量**。对于字符串,位置是绝对的 0 基索引。 +- 对于缓冲区输入,START 和 END 是从 1 开始的缓冲区位置,但返回的位置 + 默认是**相对于 START 的 0 基偏移量**(历史约定)。ABSOLUTE 非 nil + 时(0.3.0 新增),返回的位置改为原生的 1 基缓冲区位置,可以不做偏移 + 运算直接用于其他 tp 调用(`tp-set`、`tp-remove` 等)。对于字符串, + 位置始终是绝对的 0 基索引;ABSOLUTE 不改变任何行为。 - 使用 `object-intervals`(需要 Emacs 28.1+)。 - OBJECT 可以是缓冲区或字符串;nil 默认为当前缓冲区。 @@ -2682,6 +3295,24 @@ LAYER 接受以下几种形式: (tp-intervals 1 12)) ;; => ((0 5 (face bold)) (5 6 nil) (6 11 (face italic))) ;; 位置是相对 START 的偏移量;(5 6 nil) 是无属性的间隙 + +;; ABSOLUTE - 原生缓冲区坐标 +(with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (tp-set 7 12 '(face italic)) + (tp-intervals 1 12 nil t)) +;; => ((1 6 (face bold)) (6 7 nil) (7 12 (face italic))) + +;; ABSOLUTE 位置可直接回馈给其他 tp 调用 +(with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (dolist (iv (tp-intervals 1 12 nil t)) + (when (eq (plist-get (nth 2 iv) 'face) 'bold) + (tp-add (nth 0 iv) (nth 1 iv) '(help-echo "bold text")))) + (tp-at 1 'help-echo)) +;; => "bold text" ``` --- @@ -2689,13 +3320,18 @@ LAYER 接受以下几种形式: #### `tp-intervals-map` - 对区间应用函数 ```elisp -(tp-intervals-map FUNCTION START END &optional OBJECT) +(tp-intervals-map FUNCTION START END &optional OBJECT ABSOLUTE) ``` 对 OBJECT 中 START 到 END 之间的所有区间应用 FUNCTION。 -- FUNCTION 接收四个参数:interval-start、interval-end、top-props(可见层属性)和 below-props-lst(隐藏层列表)。 -- 没有属性的区间也会被访问,此时 top-props 为 nil(位置遵循与 `tp-intervals` 相同的偏移量约定)。 +- FUNCTION 接收四个参数:interval-start、interval-end、top-props(直 + 接渲染的属性,其中的 `tp-layers` 条目已被移除)和 below-props-lst + (`tp-layers` 的值:埋在被渲染顶层之下的层 plist 存储 —— 当任何层被 + 隐藏时它保存整个有序层栈;解码后的视图参见 + [`tp-layer-stack-at`](#tp-layer-stack-at---获取某位置的完整层栈))。 +- 没有属性的区间也会被访问,此时 top-props 为 nil(位置遵循与 + `tp-intervals` 相同的坐标约定,包括 0.3.0 新增的 ABSOLUTE 参数)。 - OBJECT 可以是缓冲区或字符串;nil 默认为当前缓冲区。 - 返回函数结果列表(nil 结果被移除)。 @@ -2711,6 +3347,17 @@ LAYER 接受以下几种形式: (list start end (plist-get props 'face))) 1 12)) ;; => ((0 5 bold) (5 6 nil) (6 11 italic)) + +;; ABSOLUTE - FUNCTION 接收原生缓冲区位置 +(with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (tp-set 7 12 '(face italic)) + (tp-intervals-map + (lambda (start end props belows) + (list start end (plist-get props 'face))) + 1 12 nil t)) +;; => ((1 6 bold) (6 7 nil) (7 12 italic)) ``` --- @@ -2835,7 +3482,8 @@ LAYER 接受以下几种形式: - **`tp-palette-alist`**(变量)— `(NAME . PLIST)` 形式的调色板定义 alist;调色板查询的唯一数据源。每个 PLIST 将 `:fg`、`:bg` 和 `:border` 映射到颜色。 -- **`define-tp-palette`** — 注册(或更新)一个调色板: +- **`define-tp-palette`** — 注册(或更新)一个调色板(自 0.3.0 起也可 + 使用符合前缀规范的别名 `tp-define-palette`): ```elisp (define-tp-palette my-brand @@ -2843,6 +3491,37 @@ LAYER 接受以下几种形式: :bg ("#ddf4ff" . "#1f3d5c")) ``` +- **`tp-palette-color`**(0.3.0 新增)— **首选的**调色板访问器:获取调 + 色板的 `:fg` / `:bg` / `:border` 颜色,按当前亮色/暗色主题解析。调色 + 板或键不存在时返回 nil: + + ```elisp + (tp-palette-color 'info :fg) + ;; => 亮色主题下为 "#0969da",暗色主题下为 "#58a6ff" + (tp-palette-color 'no-such-palette :fg) + ;; => nil + ``` + +- **`tp-palette-has-p`**(0.3.0 新增)— **首选的**调色板谓词:只传 + SYMBOL 时测试它是否命名了一个已注册的调色板;KIND 为 `:fg` / `:bg` / + `:border` 之一时,还要求其定义中含有该键(已定义的键在当前主题下仍 + 可能解析不出颜色 —— 在意解析后颜色时请使用 `tp-palette-color`): + + ```elisp + (list (tp-palette-has-p 'info) + (tp-palette-has-p 'info :border) + (tp-palette-has-p 'no-such-palette)) + ;; => (t t nil) + ``` + + 旧的按键便捷函数保留为兼容包装:`tp-palette-fg-color` / + `tp-palette-bg-color` / `tp-palette-border-color`(`tp-palette-color` + 的固定 KEY 变体)、`tp-palette-p`(KIND 为 nil 的 + `tp-palette-has-p`),以及带后缀名的谓词 `tp-palette-fg-p` / + `tp-palette-bg-p` / `tp-palette-fbg-p` / `tp-palette-border-p` —— 它 + 们回答的是另一个问题:像 `info-fg` 这样的*变体名*是否表示一个已注册 + 的调色板(`tp-palette` 层使用的约定)。 + - **`tp-palette-show`** — 交互式命令,显示一个画廊缓冲区,展示每个已 注册调色板及其 `-fg` / `-bg` / `-fbg` / `-border` 变体(按 `q` 退出)。 - **`tp-parse-color`** — 按当前主题解析颜色规格。接受普通颜色字符串、 @@ -3178,6 +3857,127 @@ LAYER 接受以下几种形式: - 提高同时更改多个变量时的性能 - 当多个变量相互依赖时确保状态一致 +### 层-缓冲区注册表与生命周期 + +自 0.3.0 起,响应式引擎维护一个**层→缓冲区注册表**:每条修改缓冲区并 +盖上层标记的写入路径(`tp-set` 家族、栈修改函数、match/regexp 应用函 +数)都会把目标缓冲区注册为展示该层,而一次响应式更新只访问**已注册的 +缓冲区**,不再扫描整个 `(buffer-list)`。被杀死的缓冲区会被自动清理。 +当某个层完全没有注册表条目时,会退回到旧行为做一次**学习性全量扫描**, +并把实际发现该层的每个缓冲区都注册上。 + +即使层被**隐藏**或**埋**在栈中其他层之下,更新也能到达它的区域:存储 +在 `tp-layers` 中的条目会被就地更新,因此 `tp-show-layer`(或上移该 +层)总是显露最新的值。 + +#### `tp-reactive-layer-buffers` - 查看注册表 + +```elisp +(tp-reactive-layer-buffers LAYER-NAME) +``` + +返回注册为展示 LAYER-NAME 的存活缓冲区 —— 一个列表(可能为空,表示 +"已知:没有缓冲区展示该层")—— 或者符号 `unknown`,表示该层完全没有注 +册表条目: + +```elisp +(progn + (tp-layer-reset) + (defvar reg-color "red") + (define-tp reg-layer () + :props '(face (:foreground $reg-color))) + (tp-reactive-layer-buffers 'reg-layer)) +;; => unknown ; 还从未被应用到任何缓冲区 + +(with-temp-buffer + (rename-buffer "demo-buffer" t) + (insert "Hello") + (tp-push-layer 1 6 'reg-layer) + (mapcar #'buffer-name (tp-reactive-layer-buffers 'reg-layer))) +;; => ("demo-buffer") +``` + +#### `tp-reactive-track-buffer` - 补齐字符串插入的缺口 + +```elisp +(tp-reactive-track-buffer &optional BUFFER) ; interactive +``` + +**已知缺口:**向缓冲区插入一个*已经带属性的字符串*会绕过注册缓冲区的 +缓冲区操作,因此在一次学习性全量扫描找到它之前,该缓冲区不在注册表中。 +在这类插入之后调用 `tp-reactive-track-buffer`:它会扫描 BUFFER(默认 +为当前缓冲区)中的层区域 —— 既包括被渲染的顶层,也包括埋在或隐藏在 +`tp-layers` 栈存储中的层 —— 为每个层注册该缓冲区,并按缓冲区顺序返回 +找到的层名: + +```elisp +(let ((s (tp-set "hello" 'reg-layer))) ; 带属性的字符串,游离状态 + (with-temp-buffer + (insert s) ; 绕过了注册 + (tp-reactive-track-buffer))) +;; => (reg-layer) ; 缓冲区现已为 reg-layer 注册 +``` + +#### `tp-gc-anonymous-layers` - 回收未使用的匿名层 + +```elisp +(tp-gc-anonymous-layers) ; interactive +``` + +[匿名响应式层](#匿名响应式层)是被驻留(intern)的:`equal` 相同的属性 +规格会复用其注册表条目,而不是在每次 `tp-set` 时铸造一个新层。 +`tp-gc-anonymous-layers` 取消定义所有已无注册的存活缓冲区仍在展示的驻 +留匿名层(被埋住和被隐藏的层都算作存活),并返回被回收的层名: + +```elisp +(defvar tmp-color "green") +(let ((buf (generate-new-buffer "*gc-demo*"))) + (with-current-buffer buf + (insert "Hello") + (tp-set 1 6 '(face (:foreground $tmp-color)))) ; 匿名层 + (kill-buffer buf) + (tp-gc-anonymous-layers)) +;; => (tp-anon-1) ; 被回收的层名(计数器数字会变化) +``` + +**保守的 `unknown` 语义:**注册表状态为 `unknown` 的层 —— 从未通过任 +何注册路径出现在任何缓冲区中,例如只被游离字符串引用 —— 会被刻意 +**保留**。一个层只有在至少为一个缓冲区注册过、且所有已注册的缓冲区都不再 +展示它(例如全部被杀死)之后才可回收。在插入带属性的字符串后请调用 +`tp-reactive-track-buffer`,让它们所在的缓冲区也被注册。 + +#### 最小差异的 `tp-text` 重渲染 + +响应式 `tp-text` 替换只编辑新旧文本的**差异区段**(先插入后删除),因 +此位于未变化文本中的 point 和标记保持原位;位于被编辑区段内的 point +落在编辑起点。值完全相同的更新是真正的空操作:不编辑文本、不搅动属 +性,也不触碰缓冲区的修改标志。 + +```elisp +(progn + (tp-layer-reset) + (defvar counter-val "0") + (define-tp counter-label () + :props '(tp-text $counter-val)) + (with-temp-buffer + (insert "count: 0 items") + (tp-set 8 9 'counter-label) + (let ((m (copy-marker 10))) ; 标记在 "items" 的 "i" 上 + (setq counter-val "9") ; 只有数字被编辑 + (list (buffer-substring-no-properties 1 (point-max)) + (char-after m))))) +;; => ("count: 9 items" ?i) ; 标记仍指向它原来的字符 + +;; 值相同的更新完全不触碰缓冲区 +(with-temp-buffer + (insert "count: 9 items") + (tp-set 8 9 'counter-label) + (set-buffer-modified-p nil) + (setq counter-val "9") ; 与显示的文本相同 + (buffer-modified-p)) +;; => nil +``` + ### 调试模式 tp.el 提供调试模式来帮助理解响应式更新流程: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f9c5d06..a988dab 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,7 +2,7 @@ 本文档描述 tp 库的模块分层结构与函数调用层次,从底层基础模块到上层功能模块的分层组织。 -自 0.2.0 起,原来的单文件 tp.el 已拆分为九个分层模块,`tp.el` 只作为总入口(`(require 'tp)` 依次加载全部模块,用户接口不变)。各模块的变更缘由见 [CHANGELOG.md](../CHANGELOG.md)。 +自 0.2.0 起,原来的单文件 tp.el 已拆分为九个分层模块,`tp.el` 只作为总入口(`(require 'tp)` 依次加载全部模块,用户接口不变)。0.3.0 进一步收紧了模块边界:`tp-text` 处理链下沉至 tp-ops、批量更新上收至 tp-render、层栈存储编解码与匿名层机制归位 tp-layer,钩子变量从四个减少到两个。各变更的缘由见 [CHANGELOG.md](../CHANGELOG.md)。 ## 目录 @@ -10,14 +10,15 @@ - [模块分层](#模块分层) - [tp-core.el:基础工具](#tp-coreel基础工具) - [tp-reactive.el:响应式基础设施](#tp-reactiveel响应式基础设施) - - [tp-layer.el:层定义与解析](#tp-layerel层定义与解析) - - [tp-ops.el:核心属性操作](#tp-opsel核心属性操作) + - [tp-layer.el:层定义、解析与层栈存储](#tp-layerel层定义解析与层栈存储) + - [tp-ops.el:核心属性操作与 tp-text 处理链](#tp-opsel核心属性操作与-tp-text-处理链) - [tp-search.el:模式匹配与搜索](#tp-searchel模式匹配与搜索) - [tp-render.el:响应式渲染引擎](#tp-renderel响应式渲染引擎) - [tp-stack.el:属性层栈操作](#tp-stackel属性层栈操作) - [tp-palette.el:调色板数据](#tp-paletteel调色板数据) - [tp-builtins.el:内置层与辅助工具](#tp-builtinsel内置层与辅助工具) - [钩子变量:唯一许可的反向调用](#钩子变量唯一许可的反向调用) +- [可变状态清单](#可变状态清单) - [函数调用关系图](#函数调用关系图) - [设计原则](#设计原则) @@ -32,6 +33,20 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search → tp-render → tp-stack → tp-palette → tp-builtins ``` +注意加载顺序是依赖顺序的**上界**:并非每个模块都依赖它前面的全部模块。各模块实际 `require` 的 tp- 模块如下(逐一核对自源码头部): + +| 模块 | require 的 tp- 模块 | +|------|--------------------| +| tp-core | —(仅 cl-lib、dash、seq) | +| tp-reactive | tp-core | +| tp-layer | tp-core、tp-reactive | +| tp-ops | tp-core、tp-reactive、tp-layer | +| tp-search | tp-core、tp-reactive、tp-layer、tp-ops | +| tp-render | tp-core、tp-reactive、tp-layer、tp-ops、tp-search | +| tp-stack | tp-core、tp-reactive、tp-layer(**不依赖 tp-ops / tp-search / tp-render**) | +| tp-palette | —(不依赖任何 tp- 模块,仅 subr-x) | +| tp-builtins | tp-core、tp-layer、tp-ops、tp-palette | + ``` ┌────────────────────────────────────────────────────────────────┐ │ tp.el —— 总入口,按序 require 全部模块 │ @@ -41,25 +56,28 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search │ tp-palette-show、显示缓冲辅助宏 │ ├────────────────────────────────────────────────────────────────┤ │ tp-palette.el 明/暗主题调色板数据、tp-parse-color │ +│ (独立叶模块,不依赖任何 tp- 模块) │ ├────────────────────────────────────────────────────────────────┤ -│ tp-stack.el 层栈操作(push/pop/move/merge/flatten …) │ +│ tp-stack.el 层栈操作(push/pop/move/hide/show/merge …) │ ├────────────────────────────────────────────────────────────────┤ -│ tp-render.el 响应式重渲染引擎 ──┐ │ -├──────────────────────────────────── │ ─────────────────────────┤ -│ tp-search.el tp-match-*/tp-regexp-*、tp-search、导航 │ -├──────────────────────────────────── │ ─────────────────────────┤ -│ tp-ops.el tp-set/reset/add/get/at/remove/clear │ -│ ◁╌╌ tp--tp-text-handler-function ╌╌╌╌┤ │ -├──────────────────────────────────── │ ─────────────────────────┤ -│ tp-layer.el define-tp/define-tps、层注册表与解析 │ -│ ◁╌╌ tp--layer-refresh-function ╌╌╌╌╌╌┤ │ -├──────────────────────────────────── │ ─────────────────────────┤ -│ tp-reactive.el 响应式依赖注册表、变量监听、批量队列 │ -│ ◁╌╌ tp--reactive-update-function ╌╌╌╌┤ │ -│ ◁╌╌ tp--reactive-flush-function ╌╌╌╌╌┘ │ +│ tp-render.el 响应式重渲染引擎、最小差异 tp-text 编辑、 │ +│ 批量更新(tp-with-batch-updates + flush)──┐ │ +├─────────────────────────────────────────────────────────── │ ──┤ +│ tp-search.el tp-match-*/tp-regexp-*、tp-search、导航 │ │ +├─────────────────────────────────────────────────────────── │ ──┤ +│ tp-ops.el tp-set/reset/add/get/at/remove/clear、 │ │ +│ tp-text 处理链(0.3.0 起在此,直接调用) │ │ +├─────────────────────────────────────────────────────────── │ ──┤ +│ tp-layer.el define-tp/define-tps、层注册表与解析、 │ │ +│ 层栈存储编解码、匿名层机制与 GC │ │ +│ ◁╌╌ tp--layer-refresh-function ╌╌╌╌╌╌╌╌╌╌┤ │ +├─────────────────────────────────────────────────────────── │ ──┤ +│ tp-reactive.el 响应式依赖注册表、变量监听、批量队列、 │ │ +│ 层→缓冲区注册表 │ │ +│ ◁╌╌ tp--reactive-update-function ╌╌╌╌╌╌╌╌╌┘ │ ├────────────────────────────────────────────────────────────────┤ │ tp-core.el 区间遍历、plist/face 合并引擎、 │ -│ 调试日志、$var 符号工具 │ +│ 调试日志、$var 符号工具(无可变状态) │ └────────────────────────────────────────────────────────────────┘ 实线层级:上层模块调用下层模块(require 依赖)。 @@ -67,7 +85,7 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search 由 tp-render.el 在加载时安装实现(见下文)。 ``` -早期文档把"响应式系统"画在高级 API 之下、却又让它向上调用 `tp-search-map`,与自身的分层原则矛盾。现在这一矛盾已在代码层面消除:需要向上调用的逻辑全部收拢进 `tp-render.el`(位于 `tp-search.el` 之上,可以直接调用它);下层模块(tp-reactive、tp-layer、tp-ops)通过**钩子变量**触发渲染,自身不依赖任何上层模块。 +需要"向上调用"的逻辑全部收拢在 `tp-render.el`(位于 `tp-search.el` 之上,可以直接调用它)。0.2.0 时这类反向调用靠四个钩子变量实现;0.3.0 把其中两个消除在了代码层面——`tp-text` 处理链整体下沉进 tp-ops(`tp-set` 等直接调用,不再需要 `tp--tp-text-handler-function`;只加载到 tp-ops 的部分加载也能得到可用的 `tp-text` 文本替换),批量刷新整体上收进 tp-render(`tp--flush-batch-updates` 直接调用 `tp--reactive-flush-entry`,不再需要 `tp--reactive-flush-function`)。剩下的两个钩子对应真正源自下层的事件:变量监听器触发(tp-reactive)与层重定义触发(tp-layer)。 --- @@ -75,14 +93,14 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search ### tp-core.el:基础工具 -最底层模块,不依赖任何其他 tp 模块,提供区间遍历、合并引擎与调试能力。 +最底层模块,不依赖任何其他 tp 模块,提供区间遍历、合并引擎与调试能力。0.3.0 起 tp-core **不再持有任何可变状态**(匿名层计数器已迁至 tp-layer;仅剩 `tp-debug-mode` / `tp-debug-echo` 两个 defcustom 用户选项)。 #### 区间操作 | 函数 | 描述 | 主要调用者 | |------|------|--------| -| `tp-intervals` | 获取区域内文本属性区间列表(裁剪到 [START, END)) | tp-intervals-map, tp-get | -| `tp-intervals-map` | 对区间应用函数 | 多个属性/层操作函数 | -| `tp--map-intervals` | 共享的裁剪式区间遍历引擎 | tp-intervals-map, tp-ops/tp-stack 的区域操作 | +| `tp-intervals` | 获取区域内文本属性区间列表(裁剪到 [START, END);可选 ABSOLUTE 参数返回缓冲区原生坐标,默认仍为相对坐标) | tp-intervals-map, tp-get | +| `tp-intervals-map` | 对区间应用函数(同样支持 ABSOLUTE) | 多个属性/层操作函数 | +| `tp--map-intervals` | 共享的裁剪式区间遍历引擎 | tp-intervals-map, tp-ops/tp-stack 的区域操作, tp-reactive 的缓冲区扫描 | | `tp-plist` | 获取区域中合并后的所有属性 | 用户 API | | `tp-empty-p` | 检查对象是否没有文本属性 | 用户 API | @@ -94,6 +112,7 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search | `tp--merge-face-values` | 合并两个 face 值 | 合并引擎内部 | | `tp--merge-duplicate-keys` | 合并 plist 中的重复键 | tp--parse-args | | `tp--parse-face-list` | 解析 face 列表 | 合并引擎内部 | +| `tp--merge-string-props-into-plist` | 将字符串内嵌属性并入 plist | tp-ops 的 tp-text 处理链 | | `tp--get-nested` | 按路径获取嵌套属性值 | tp-get, tp-at | `tp-face-properties`(常量,`'(face font-lock-face mouse-face)`)定义参与 face 感知合并的属性家族。 @@ -122,98 +141,139 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search ### tp-reactive.el:响应式基础设施 -只依赖 tp-core。维护响应式依赖注册表、变量监听器与批量更新队列;**不包含任何渲染逻辑**,重渲染通过钩子变量委托给 tp-render.el。 +只依赖 tp-core。维护响应式依赖注册表、变量监听器、批量更新队列与 0.3.0 新增的**层→缓冲区注册表**;**不包含任何渲染逻辑**,重渲染通过钩子变量委托给 tp-render.el。批量更新的队列(`tp--batch-update-pending`、`tp--queue-batch-update`)定义在这里,但 `tp-with-batch-updates` 宏与刷新逻辑自 0.3.0 起位于 tp-render.el。 #### 依赖注册与管理 | 函数/变量 | 描述 | |------|------| | `tp-reactive-deps` | 变量 → 依赖它的层及属性 的注册表 | | `tp--register-reactive-deps` | 注册响应式依赖 | -| `tp--unregister-reactive-deps` | 取消注册依赖(含 watchers/computed/data) | +| `tp--unregister-reactive-deps` | 取消注册依赖(含 watchers/computed/data,并移除该层的缓冲区注册表条目) | | `tp--layer-has-reactive-deps-p` | 层是否有响应式依赖 | | `tp--register-layer-watchers` / `tp--unregister-layer-watchers` | 注册/清除 `:watch` 回调 | | `tp--register-layer-computed` / `tp--unregister-layer-computed` | 注册/清除 `:compute` 计算属性 | | `tp--register-layer-data` / `tp--unregister-layer-data` | 注册/清除 `:data` 变量 | | `tp--apply-initial-computed` | 计算 `:compute` 的初始值 | | `tp--ensure-reactive-variables` | 确保 `$var` 对应的变量已定义 | -| `tp-reactive-reset` | 重置全部响应式注册表 | +| `tp-reactive-reset` | 重置全部响应式注册表(含批量队列与层→缓冲区注册表) | -#### 变量监听与批量更新 -| 函数/宏 | 描述 | +#### 层→缓冲区注册表(0.3.0) +响应式更新不再全量扫描 `(buffer-list)`:每条会写入 `tp-name` 的缓冲区路径(tp-set 家族、栈变更函数、match/regexp 应用器)都把目标缓冲区登记到注册表,更新时只访问登记过的缓冲区。 + +| 函数/变量 | 描述 | +|------|------| +| `tp--layer-buffers` | 哈希表(`:test equal`):层名 → 展示该层的缓冲区列表。键存在但值为空表示"已知:无缓冲区展示该层",与键不存在(`unknown`)严格区分 | +| `tp-reactive--register-layer-buffer` | 幂等登记(公开写入口,tp-ops/tp-search/tp-stack 各自的注册助手最终都调用它);首次使用时安装 `kill-buffer-hook` 清理器 | +| `tp-reactive-layer-buffers` | 查询某层的已登记存活缓冲区,或返回符号 `unknown`;惰性剔除已死缓冲区 | +| `tp-reactive--buffer-layer-names` | 栈感知的缓冲区扫描:直接 `tp-name` 与 `tp-layers` 栈存储内的层(被覆盖或被隐藏)都算在场。`tp-reactive-track-buffer` 与匿名层 GC 的存活检查共用它 | +| `tp-reactive-track-buffer` | 交互命令:扫描缓冲区并登记其中的全部层。用于弥补"插入已带属性的字符串"绕过登记路径的已知缺口 | +| `tp-reactive--prune-killed-buffer` / `tp-reactive--install-kill-buffer-hook` | kill-buffer 时从注册表剔除死缓冲区(条目保留为空列表,即"已知:无") | + +对 `unknown` 层,tp-render 的更新走一次**学习性**全扫描并登记实际找到的缓冲区;一处都没找到的层刻意保持 `unknown`,以便之后经非登记路径(如字符串插入)出现时仍能被下次扫描发现。 + +#### 变量监听与批量队列 +| 函数 | 描述 | |------|------| | `tp--reactive-variable-watcher` | `add-variable-watcher` 回调;调用 `:watch` 后经 `tp--reactive-update-function` 委托重渲染 | | `tp--invoke-layer-watchers` | 调用层的 `:watch` 回调 | -| `tp-with-batch-updates` | 批量更新宏 | -| `tp--queue-batch-update` | 将更新加入待处理队列 | -| `tp--flush-batch-updates` | 刷新队列,经 `tp--reactive-flush-function` 委托重渲染 | +| `tp--queue-batch-update` | 将更新加入待处理队列 `tp--batch-update-pending`(刷新在 tp-render) | -钩子变量:`tp--reactive-update-function`、`tp--reactive-flush-function`(定义于此,由 tp-render.el 安装)。 +钩子变量:`tp--reactive-update-function`(定义于此,由 tp-render.el 安装)。 --- -### tp-layer.el:层定义与解析 +### tp-layer.el:层定义、解析与层栈存储 -依赖 tp-core、tp-reactive。提供 `define-tp` / `define-tps` 宏、层注册表、层名解析,以及层栈的数据结构原语。 +依赖 tp-core、tp-reactive。提供 `define-tp` / `define-tps` 宏、层注册表、层名解析,以及 0.3.0 归位至此的**层栈存储编解码**与**匿名层完整生命周期**(铸造、驻留、注销、GC)。 #### 层定义 | 函数/宏 | 描述 | 依赖 | |---------|------|------| -| `define-tp` | 定义单个自定义文本属性(层) | tp--define-layer-internal | -| `define-tps` | 定义自定义文本属性组(层组);别名 `define-tp-group` | tp--define-layer-group-internal | +| `define-tp` | 定义单个自定义文本属性(层);别名 `tp-define-layer` | tp--define-layer-internal | +| `define-tps` | 定义自定义文本属性组(层组);别名 `define-tp-group`、`tp-define-group` | tp--define-layer-group-internal | | `tp--define-layer-internal` | 层定义的运行时实现 | tp--parse-define-layer-args, tp--collect-reactive-symbols, tp--ensure-reactive-variables, tp--register-*, tp--layer-refresh | | `tp--parse-define-layer-args` | 解析 `:props` / `:data` / `:compute` / `:watch` / `:transform` | - | | `tp--parse-layer-group-element` | 解析层组元素 | tp--layer-group-element-format | | `tp--define-layer-from-parsed` | 从解析结果定义层 | (与 define-tp 类似的依赖) | | `tp--check-layer-cycle` | 检测循环层引用并报错 | tp--layer-expansion-stack | -| `tp--anonymous-layer-name-for` | 匿名响应式层的驻留(`equal` 的 props 复用注册项) | - | + +0.3.0 起参数化层/层组的 ARGLIST 可以声明**任意个**参数(此前仅限一个);`(LAYER ARG1 ... ARGN)` 与包裹形式 `(LAYER (ARG1 ... ARGN))` 在 `tp-set` 与 `tp-put-layer` 规格中均可用,实参数量不匹配会报出点名该层与两个数量的清晰错误。 #### 注册表与查询 | 函数/变量 | 描述 | |------|------| | `tp-layer-alist` / `tp-layer-groups` / `tp-layer-transforms` | 层、层组、转换函数注册表 | +| `tp--group-generated-layers` | 层组 → 其定义生成的层 的注册表(组重定义/注销时随之清理) | | `tp--set-layer-props` / `tp--set-group-layers` | 写入注册表 | | `tp-layer-props` / `tp-group-props` | 获取层/层组属性(`&optional INCLUDE-TP-NAME`,默认不含 `tp-name`;返回副本) | -| `tp-layer-props-with-arg` / `tp-group-props-with-arg` | 参数化层/层组的属性求值 | +| `tp-layer-props-with-arg` / `tp-group-props-with-arg` | 单参数形式(0.3.0 起是 -with-args 的薄封装) | +| `tp-layer-props-with-args` / `tp-group-props-with-args` | 多参数形式:ARGS 按位置绑定到层参数 | +| `tp-layer-arglist` | 返回参数化层的形参表副本(非参数化层返回 nil) | | `tp-layer-parameterized-p` / `tp-group-parameterized-p` | 是否参数化 | -| `tp-layer-reset` | 重置层系统 | -| `tp-undefine-layer` / `tp-undefine-group` | 删除层/层组(含其响应式依赖与转换) | +| `tp-describe-layer` | 交互命令:在 help 缓冲区展示层的存储格式、形参表、原始定义体、展开属性、响应式依赖、transform 与所属层组(数据采集在 `tp--describe-layer-data`) | +| `tp-layer-reset` | 重置层系统(连带调用 `tp-reactive-reset`;见[可变状态清单](#可变状态清单)) | +| `tp-undefine-layer` / `tp-undefine-group` | 删除层/层组(含其响应式依赖、转换与匿名层注册表条目) | #### 属性解析 | 函数 | 描述 | 依赖 | |------|------|------| -| `tp--resolve-props` | 解析属性(展开层名、`$var`、注册依赖) | tp-layer-props, tp--collect-reactive-symbols, tp--resolve-reactive-symbols, tp--register-reactive-deps | +| `tp--resolve-props` | 解析属性(展开层名、多参数规格、`$var`、注册依赖、驻留匿名层) | tp-layer-props(-with-args), tp--collect-reactive-symbols, tp--resolve-reactive-symbols, tp--anonymous-layer-name-for, tp--register-reactive-deps | | `tp--expand-layer-in-plist` | 展开 plist 中的层名键 | tp--is-layer-name-p | -| `tp--expand-layer-to-props-list` | 层名展开为属性列表 | tp--check-layer-cycle | -#### 层栈数据结构原语 +#### 匿名层机制与 GC(0.3.0 归位/新增) +| 函数/变量 | 描述 | +|------|------| +| `tp--anonymous-layer-counter` | 匿名层名计数器。**刻意不被任何 reset 清零**:脱离缓冲区的字符串可能仍携带旧的 `tp-anon-N` 属性值,计数器单调递增保证新铸名字永不与之混淆 | +| `tp--generate-anonymous-layer-name` | 生成唯一的 `tp-anon-N` 符号 | +| `tp--anonymous-layer-registry` | 匿名响应式层驻留表:`equal` 的 props 规格复用既有注册项 | +| `tp--anonymous-layer-name-for` | 驻留查询/铸造入口 | +| `tp--buffer-has-layer-region-p` | 栈感知的存活检查:直接 `tp-name` 或 `tp-layers` 内(被覆盖/被隐藏)皆算存活 | +| `tp-gc-anonymous-layers` | 交互命令:回收已无任何已登记存活缓冲区展示的匿名层;注册表状态为 `unknown` 的层(可能仅被游离字符串引用)保守保留 | + +#### 层栈存储编解码 +层栈在原始文本属性上的编码/解码知识集中在这里,tp-stack(栈操作)与 tp-render(响应式写穿)都向下调用它,互不 require。 + | 函数 | 描述 | |------|------| -| `tp--normalize-layer-spec` | 规范化层规格 | -| `tp--get-layer-stack` | 获取位置的层栈 | -| `tp--build-layer-props` | 从层列表构建属性 | -| `tp--layer-stack-to-list` | 将层栈转换为列表 | +| `tp--normalize-layer-spec` | 规范化层规格(含多参数 `(LAYER ARG1 ... ARGN)`) | +| `tp--build-layer-props` / `tp--layer-stack-to-list` | 旧式编解码原语(无隐藏层语义) | +| `tp--stack-hidden-p` | 层 plist 是否带 `tp-hidden` 标志 | +| `tp--stack-props-to-list` | 原始属性 → 有序层列表(顶层在前,含隐藏层)。有隐藏层时 `tp-layers` 持有完整栈,直接属性只是最顶可见层的渲染缓存 | +| `tp--stack-build-props` | 有序层列表 → 原始属性。单层栈不携带 `tp-layers`;含隐藏层时切换为"完整栈 + 渲染缓存"存储模式(全部隐藏时不渲染任何层属性) | | `tp--get-layer-by-idx-or-name` | 通过索引或名称查找层 | 钩子变量:`tp--layer-refresh-function`(定义于此,由 tp-render.el 安装为 `tp--update-layer-regions`);`tp--layer-refresh` 是它的调用入口,层重定义后经它触发已应用区域的重渲染。 --- -### tp-ops.el:核心属性操作 +### tp-ops.el:核心属性操作与 tp-text 处理链 -依赖 tp-core、tp-layer。面向用户的核心属性读写函数,直接调用 Emacs 原生文本属性 API。 +依赖 tp-core、tp-reactive、tp-layer。面向用户的核心属性读写函数,直接调用 Emacs 原生文本属性 API。0.3.0 起 `tp-text` 处理链从 tp-render 下沉至此,`tp-set` 等**同模块直接调用**它(不再经钩子变量)——因此只加载到 tp-ops 的部分加载也能得到可用的 `tp-text` 文本替换。 #### 参数解析 | 函数 | 描述 | 调用者 | |------|------|--------| -| `tp--parse-args` | 解析灵活的调用格式(整串/区域/层名) | tp-set, tp-reset, tp-add | +| `tp--parse-args` | 解析灵活的调用格式(整串/区域/层名/多参数层) | tp-set, tp-reset, tp-add | | `tp--apply-props-to-string` | 字符串路径的属性应用 | tp-set, tp-reset, tp-add | +| `tp--ops-register-layer-buffer` | 应用带 `tp-name` 的属性到缓冲区后,登记到层→缓冲区注册表 | tp-set, tp-reset, tp-add | + +#### tp-text 处理链(0.3.0 自 tp-render 迁入) +| 函数 | 描述 | +|------|------| +| `tp--handle-tp-text-property` | `tp-text` 属性的总入口:初始化/替换文本、双向同步响应式变量 | +| `tp--tp-text-replace` | 执行文本替换(缓冲区与字符串两条路径) | +| `tp--tp-text-transform` | 应用层的 `:transform`(首次渲染同样生效) | +| `tp--find-tp-text-reactive-var` | 找到层 `tp-text` 绑定的响应式变量 | +| `tp--merge-embedded-props` | 合并 tp-text 字符串内嵌属性与外部属性 | +| `tp--apply-reactive-text-props` | 把结果属性应用到替换文本(值未变的区段跳过写入,保持 buffer-modified 状态) | +| `tp--put-text-property-unless-equal` | 仅在值确实变化时写属性 | #### 设置属性 | 函数 | 描述 | 依赖 | 被依赖 | |------|------|------|--------| -| `tp-set` | 设置文本属性(保留其他属性) | tp--parse-args, tp--handle-tp-text | tp-match-set, 层操作 | -| `tp-reset` | 完全替换所有文本属性 | tp--parse-args, tp--handle-tp-text | tp-match-reset | -| `tp-add` | 深度合并属性 | tp--parse-args, tp--deep-merge-plist, tp--prepend-face | tp-match-add | +| `tp-set` | 设置文本属性(保留其他属性) | tp--parse-args, tp--handle-tp-text-property, tp--ops-register-layer-buffer | tp-match-set, 层操作 | +| `tp-reset` | 完全替换所有文本属性 | 同上 | tp-match-reset | +| `tp-add` | 深度合并属性 | 同上 + tp--deep-merge-plist, tp--prepend-face | tp-match-add | #### 获取属性 | 函数 | 描述 | 依赖 | 被依赖 | @@ -226,39 +286,39 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search | 函数 | 描述 | 依赖 | 被依赖 | |------|------|------|--------| | `tp-remove` | 移除属性或子属性 | tp--remove-property, tp--remove-sub, tp--remove-*-from-string | 用户 API | -| `tp-clear` | 清除所有属性 | - | 用户 API | - -钩子变量:`tp--tp-text-handler-function`(定义于此,由 tp-render.el 安装为 `tp--handle-tp-text-property`);`tp--handle-tp-text` 是它的调用入口,未安装时 `tp-text` 属性按普通属性处理。 +| `tp-clear` | 清除所有属性(显式返回 nil) | - | 用户 API | --- ### tp-search.el:模式匹配与搜索 -依赖 tp-core、tp-layer、tp-ops。提供模式匹配式属性应用、属性搜索与导航。 +依赖 tp-core、tp-reactive、tp-layer、tp-ops(0.3.0 新增 tp-reactive 依赖:应用器写入缓冲区后经 `tp--search-register-layer-buffer` 登记层→缓冲区注册表)。提供模式匹配式属性应用、属性搜索与导航。 #### 模式匹配 | 函数 | 描述 | 依赖 | |------|------|------| -| `tp-match-set` / `tp-match-reset` / `tp-match-add` | 在字符串匹配处设置/重置/合并属性 | tp--match-apply | -| `tp-regexp-set` / `tp-regexp-reset` / `tp-regexp-add` | 在正则匹配处设置/重置/合并属性 | tp--regexp-apply | +| `tp-match-set` / `tp-match-reset` / `tp-match-add` | 在字符串匹配处设置/重置/合并属性;0.3.0 起接受 START/END 界限(视同只存在该部分;颠倒的界限自动交换) | tp--match-apply | +| `tp-regexp-set` / `tp-regexp-reset` / `tp-regexp-add` | 在正则匹配处设置/重置/合并属性;0.3.0 起额外接受 SUBEXP(属性作用于每个匹配的该捕获组;超出组数报清晰错误) | tp--regexp-apply | | `tp--match-apply` / `tp--regexp-apply` | 字面/正则匹配的入口(含多模式支持) | tp--pattern-apply | -| `tp--pattern-apply` / `tp--pattern-apply-single` | 共享的模式匹配引擎(空模式/零宽模式安全) | tp-set/tp-reset/tp-add 风格的 apply-fn | -| `tp--deep-merge-apply` / `tp--reset-apply` | 传给引擎的合并/重置回调 | tp--deep-merge-plist 等 | +| `tp--pattern-apply` / `tp--pattern-apply-single` | 共享的模式匹配引擎(空模式/零宽模式安全;承载 START/END/SUBEXP) | tp-set/tp-reset/tp-add 风格的 apply-fn | +| `tp--deep-merge-apply` / `tp--reset-apply` | 传给引擎的合并/重置回调(缓冲区路径顺带登记注册表) | tp--deep-merge-plist, tp--search-register-layer-buffer | +| `tp--search-register-layer-buffer` | 登记助手,转发到 `tp-reactive--register-layer-buffer` | tp-reactive | #### 搜索和导航 | 函数 | 描述 | 依赖 | |------|------|------| -| `tp-search-forward` | 向前搜索属性 | text-property-search-forward | -| `tp-search-backward` | 向后搜索属性 | tp--property-search-backward | -| `tp--property-search-backward` | 带等值谓词的向后搜索(与向前语义对称) | text-property-search-backward | -| `tp-forward` | 向前搜索 N 次并移动点 | tp-search-forward | -| `tp-backward` | 向后搜索 N 次并移动点 | tp-search-backward | +| `tp-forward` | 向前搜索 N 次并移动点;0.3.0 起接受 PREDICATE 与 NOT-CURRENT(默认保持 0.2.0 的对称 `equal` 匹配契约) | text-property-search-forward | +| `tp-backward` | 向后搜索 N 次并移动点(与向前语义对称,同样新增 PREDICATE/NOT-CURRENT) | tp--property-search-backward | +| `tp--property-search-backward` | 带谓词的向后搜索引擎 | text-property-search-backward | +| `tp--property-match-p` | 谓词归一化(nil/t → `equal`;函数 → `(funcall PRED VALUE PROP-VALUE)`) | - | +| `tp--string-property-matches` | 字符串路径的按段匹配收集器 | - | | `tp-search` | 收集所有匹配区间 | tp-intervals 等 | +| `tp-search-forward` / `tp-search-backward` | **已废弃(0.3.0,make-obsolete)**:裸封装原语,nil-PREDICATE 默认语义与库内 `equal` 匹配相悖;请改用 `tp-forward` / `tp-backward`,或直接用 Emacs 原语 | text-property-search-* | #### 遍历与替换 | 函数 | 描述 | 依赖 | |------|------|------| -| `tp-forward-do` / `tp-backward-do` | 向前/向后搜索并对匹配执行函数 | tp--forward-do / tp--backward-do | +| `tp-forward-do` / `tp-backward-do` | 向前/向后搜索并在第 TIMES 个匹配处执行函数(同样透传 PREDICATE/NOT-CURRENT) | tp--forward-do / tp--backward-do | | `tp--forward-do` / `tp--backward-do` | 单方向遍历的内部实现 | tp--replace-match-text | | `tp-search-map` | 对所有匹配应用函数(FUNCTION 接收 TEXT &optional START END IDX) | tp--search-do | | `tp--search-do` | 搜索遍历的内部实现 | tp--replace-match-text | @@ -268,51 +328,64 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search ### tp-render.el:响应式渲染引擎 -依赖 tp-core、tp-reactive、tp-layer、tp-ops、tp-search。这是唯一"知道"渲染如何进行的模块:它可以直接调用 `tp-search-map`、`tp-add` 等前置模块的函数,并在加载末尾把自己的入口函数**安装**进下层模块预留的钩子变量。 +依赖 tp-core、tp-reactive、tp-layer、tp-ops、tp-search。这是唯一"知道"渲染如何进行的模块:它直接调用 `tp-search-map`、`tp--tp-text-transform`、`tp--apply-reactive-text-props`(后两者位于 tp-ops——这条 require 是真实的下行调用,不只是加载顺序),并在加载末尾把自己的入口函数**安装**进下层模块预留的两个钩子变量。0.3.0 起批量更新宏与刷新逻辑也位于此。 + +#### 缓冲区遍历(0.3.0:注册表驱动) +| 函数 | 描述 | 依赖 | +|------|------|------| +| `tp--render-visit-buffer` | 单缓冲区访问接缝(测试可包裹它统计访问次数) | tp-with-current-buffer | +| `tp--map-layer-buffers` | 在可能展示该层的缓冲区中执行更新:WHERE 为缓冲区(setq-local)时只走它;否则查注册表只访问已登记缓冲区;`unknown` 层回退为一次学习性 `(buffer-list)` 全扫描并登记实际命中的缓冲区 | tp-reactive-layer-buffers, tp--buffer-has-layer-region-p | #### 重渲染 | 函数 | 描述 | 依赖 | |------|------|------| -| `tp--update-layer-regions` | 重渲染携带某层的所有文本区域(替换该层自己的属性键,保留其他来源属性) | tp--layer-render-props, tp-search-map | +| `tp--update-layer-regions` | 重渲染携带某层的所有文本区域(替换该层自己的属性键,保留其他来源属性),并**写穿**到 `tp-layers` 栈存储 | tp--layer-render-props, tp-search-map, tp--write-layer-through-stack-storage | +| `tp--write-layer-through-stack-storage` | 把新属性写进栈存储里该层的条目(被覆盖或被隐藏的副本也保持最新,`tp-show-layer` 后渲染当前值而非陈旧快照;值未变的段不触碰缓冲区) | tp--stack-props-to-list, tp--stack-build-props [tp-layer] | +| `tp--merge-props-into-stack-entry` | 更新栈条目的键,保留其 `tp-hidden` 标志与栈位置 | - | | `tp--update-layer-computed` | 更新 `:compute` 计算属性(nil 值可正常传播) | tp--resolve-reactive-symbols, tp--set-layer-props | | `tp--layer-render-props` / `tp--layer-reactive-props` | 求取层的渲染属性 | tp-layer-props | #### 响应式文本(tp-text) | 函数 | 描述 | 依赖 | |------|------|------| -| `tp--handle-tp-text-property` | 处理 `tp-text` 属性(文本替换) | tp--tp-text-replace | -| `tp--update-reactive-text` | 变量变化后更新响应式文本 | tp--replace-reactive-text-in-buffer | -| `tp--replace-reactive-text-in-buffer` | 在缓冲区中替换响应式文本 | - | -| `tp--tp-text-transform` | 应用 `:transform` 转换(首次渲染同样生效) | tp-layer-transforms | +| `tp--update-reactive-text` | 变量变化后更新响应式文本 | tp--replace-reactive-text-in-buffer, tp--map-layer-buffers | +| `tp--replace-reactive-text-in-buffer` | 在缓冲区中替换响应式文本(0.3.0:**最小差异编辑**——修剪公共前后缀只编辑差异区段,且先插入后删除,未变文本内的点位与标记不动;文本相同的更新完全不触碰缓冲区) | tp--edit-region-minimal-diff | +| `tp--edit-region-minimal-diff` | 最小差异编辑原语 | - | +| `tp--pos-holds-layer-in-storage-only-p` | 某位置的层是否只存在于栈存储(隐藏/被覆盖,跳过可见文本替换) | - | + +#### 批量更新(0.3.0 自 tp-reactive 迁入) +| 函数/宏 | 描述 | +|------|------| +| `tp-with-batch-updates` | 批量更新宏:BODY 内的多次变量修改合并为一次刷新(队列变量仍在 tp-reactive,宏向下 let 绑定它们) | +| `tp--flush-batch-updates` | 刷新队列,按层去重后**直接调用** `tp--reactive-flush-entry`(不再经钩子) | +| `tp--reactive-flush-entry` | 单条刷新的工作函数(属性更新或 tp-text 替换) | #### 引擎入口与钩子安装 | 函数 | 描述 | |------|------| -| `tp--reactive-apply-update` | 变量变化的完整处理:更新 computed、合并层定义、重渲染或入批量队列(嵌套写入经队列而非递归)。安装为 `tp--reactive-update-function` | -| `tp--reactive-flush-entry` | 批量队列刷新时的重渲染入口。安装为 `tp--reactive-flush-function` | +| `tp--reactive-apply-update` | 变量变化的完整处理:更新 computed、合并层定义、重渲染或入批量队列(嵌套写入经队列而非递归)。尾部刷新置于 `unwind-protect` 清理段中,重渲染抛错也不会把队列条目困死。安装为 `tp--reactive-update-function` | -加载末尾执行安装: +加载末尾执行安装(与源码逐字一致): ```elisp (setq tp--reactive-update-function #'tp--reactive-apply-update) -(setq tp--reactive-flush-function #'tp--reactive-flush-entry) -(setq tp--tp-text-handler-function #'tp--handle-tp-text-property) -(setq tp--layer-refresh-function #'tp--update-layer-regions) +(setq tp--layer-refresh-function #'tp--update-layer-regions) ``` --- ### tp-stack.el:属性层栈操作 -依赖 tp-core、tp-layer、tp-ops。所有栈变更函数建立在共享的裁剪式区域遍历之上,区域操作不会影响 [START, END) 之外的文本。 +依赖 tp-core、tp-reactive、tp-layer——**不依赖 tp-ops**(0.2.0 的幻影依赖已在 0.3.0 移除,独立字节编译无警告)。所有栈变更函数建立在共享的裁剪式区域遍历之上,区域操作不会影响 [START, END) 之外的文本;栈的存储编解码在 tp-layer(向下调用)。0.3.0 起所有栈变更函数**返回实际修改的属性段数量**(0 表示无匹配;`tp-put-layer`/`tp-push-layer` 例外,仍返回 OBJECT 或 `(START . END)`),每次改写后经 `tp--stack-register-layers` 登记层→缓冲区注册表。字符串形式**原地修改**字符串(与 `tp-set` 的复制语义不同,各函数 docstring 均有警示)。 #### 内部助手 | 函数 | 描述 | 依赖 | |------|------|------| | `tp--parse-layer-args` | 解析层操作的灵活参数 | - | -| `tp--stack-map-region` | 按区间遍历区域内层栈的共享引擎 | tp--map-intervals 风格遍历 | -| `tp--stack-build-props` | 从层列表构建栈属性(单层栈不携带 `tp-layers`) | - | -| `tp--put-layer-specs` | 展开层规格(层名/内联 plist/层名列表/参数化/层组) | tp--normalize-layer-spec, tp-group-props(-with-arg) | +| `tp--plist-remove` | 返回去掉某键的 plist 副本 | - | +| `tp--stack-map-region` | 按区间遍历区域内层栈的共享引擎(解码经 tp--stack-props-to-list,含隐藏层) | tp--map-intervals [tp-core], tp--stack-props-to-list [tp-layer] | +| `tp--stack-register-layers` | 把新栈中每个带 `tp-name` 的层(含被覆盖与隐藏的)登记到缓冲区注册表 | tp-reactive--register-layer-buffer [tp-reactive] | +| `tp--put-layer-specs` | 展开层规格(层名/内联 plist/层名列表/参数化/层组) | tp--normalize-layer-spec, tp-group-props(-with-arg) [tp-layer] | | `tp--move-layer-in-stack` | 在栈中移动层 | tp--get-layer-by-idx-or-name | | `tp--raise-layer-in-stack` | 在栈中上下移动层 | tp--move-layer-in-stack | | `tp--switch-layers-in-stack` | 交换两个层的位置 | tp--get-layer-by-idx-or-name | @@ -320,25 +393,29 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search #### 层操作(公开 API) | 函数 | 描述 | 依赖 | |------|------|------| -| `tp-put-layer` | 在指定索引放置层(区域局部) | tp--put-layer-specs, tp--stack-map-region | -| `tp-push-layer` | 将层推到顶部 | tp-put-layer | +| `tp-put-layer` | 在指定索引放置层(区域局部;0.3.0 新增尾参 NOERROR:未定义层名返回 nil 而非报错) | tp--put-layer-specs, tp--stack-map-region | +| `tp-push-layer` | 将层推到顶部(同样支持 NOERROR) | tp-put-layer | | `tp-delete-layer` | 删除层 | tp--stack-map-region | | `tp-pop-layer` | 弹出顶层 | tp-delete-layer | | `tp-move-layer` | 移动层到指定位置 | tp--move-layer-in-stack, tp--stack-map-region | -| `tp-raise-layer` | 上移/下移层 | tp--raise-layer-in-stack, tp--stack-map-region | -| `tp-rotate-layer` | 轮换层 | tp-move-layer | -| `tp-pin-layer` | 将层置顶 | tp-move-layer | +| `tp-raise-layer` | 上移层 | tp--raise-layer-in-stack, tp--stack-map-region | +| `tp-lower-layer` | 下移层(0.3.0 新增,tp-raise-layer 的镜像) | tp--raise-layer-in-stack, tp--stack-map-region | +| `tp-rotate-layer` | 轮换层(0.3.0:规范顺序 `(START END DIRECTION [COUNT] [OBJECT])`,凭 `up`/`down` 符号无歧义分派;旧顺序永久兼容;单趟栈旋转实现) | tp--stack-map-region | +| `tp-pin-layer` | 将层一次性移到栈顶(不阻止后续 push 覆盖) | tp-move-layer | | `tp-switch-layer` | 交换两个层 | tp--switch-layers-in-stack, tp--stack-map-region | -| `tp-merge-layers` | 合并多个层(显式 nil 值保留) | tp--merge-layer-props, tp--stack-map-region | -| `tp-flatten-layers` | 扁平化所有层 | tp--merge-layer-props, tp--stack-map-region | +| `tp-hide-layer` | 隐藏层(0.3.0 新增):层留在栈中、继续接收响应式更新但不渲染;隐藏可见顶层则显露下一可见层;全部隐藏时文本仅剩 `tp-layers` 记账属性 | tp--stack-map-region, tp--stack-build-props [tp-layer] | +| `tp-show-layer` | 取消隐藏(0.3.0 新增) | 同上 | +| `tp-merge-layers` | 合并多个层(显式 nil 值保留;隐藏的匹配层不贡献属性,全部匹配层均隐藏时合并结果保持隐藏) | tp--merge-layer-props, tp--stack-map-region | +| `tp-flatten-layers` | 扁平化所有层(只合并可见层;全部隐藏时得到裸文本) | tp--merge-layer-props, tp--stack-map-region | #### 层查询 | 函数 | 描述 | 依赖 | |------|------|------| -| `tp-layer-list` | 列出所有层名称 | tp--stack-map-region | -| `tp-layer-count` | 计算层数量 | tp--stack-map-region | +| `tp-layer-list` | 列出所有层名称(含隐藏层) | tp--stack-map-region | +| `tp-layer-count` | 计算层数量(含隐藏层) | tp--stack-map-region | | `tp-layer-exists-p` | 检查层是否存在 | tp-layer-list | -| `tp-layer-top` | 获取顶层名称(覆盖整个请求区域) | tp--stack-map-region | +| `tp-layer-top` | 获取顶层名称(覆盖整个请求区域;按栈序报告最顶层,即使它被隐藏) | tp--stack-map-region | +| `tp-layer-stack-at` | 单个位置的完整有序层栈:`(NAME . PROPS)` 列表,顶层在前,隐藏层以 PROPS 中的 `tp-hidden t` 标识(0.3.0 新增) | tp--stack-props-to-list [tp-layer] | | `tp-region-layer-props` | 获取区域中特定层的属性 | tp--stack-map-region | #### 层属性操作 @@ -351,16 +428,18 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search ### tp-palette.el:调色板数据 -只依赖 tp-core(及 subr-x)。明/暗主题双值调色板系统,`tp-palette-alist` 是唯一数据源。 +**不依赖任何 tp- 模块**(仅 subr-x),是独立的叶模块。明/暗主题双值调色板系统,`tp-palette-alist` 是唯一数据源。 | 函数/宏/变量 | 描述 | |------|------| -| `define-tp-palette` | 定义调色板(重定义立即生效) | +| `define-tp-palette` | 定义调色板(重定义立即生效);别名 `tp-define-palette` | | `tp-palette-alist` | 调色板注册表(唯一数据源) | | `tp-parse-color` | 解析颜色规格(支持 `("light" . "dark")` 及单边 cons) | | `tp-theme-dark-p` / `tp-theme-light-p` | 当前主题判断 | -| `tp-palette-fg-color` / `tp-palette-bg-color` / `tp-palette-border-color` | 取前景/背景/边框色 | -| `tp-palette-p` / `tp-palette-fg-p` / `tp-palette-bg-p` / `tp-palette-fbg-p` / `tp-palette-border-p` | 调色板谓词 | +| `tp-palette-color` | 通用的主题解析取色器(0.3.0 新增的首选查询入口) | +| `tp-palette-has-p` | 谓词整合入口:KIND 取 `:fg`/`:bg`/`:border`/nil(0.3.0 新增) | +| `tp-palette-fg-color` / `tp-palette-bg-color` / `tp-palette-border-color` | 取前景/背景/边框色(兼容便捷函数) | +| `tp-palette-p` / `tp-palette-fg-p` / `tp-palette-bg-p` / `tp-palette-fbg-p` / `tp-palette-border-p` | 调色板谓词(兼容便捷函数) | | `tp-palette-pure` | 取纯色值 | --- @@ -374,20 +453,46 @@ tp-core → tp-reactive → tp-layer → tp-ops → tp-search | 内置层 | `tp-palette`、`tp-fg`、`tp-bg`、`tp-button`、`tp-underline`、`tp-delete`、`tp-link`、`tp-space`、`tp-headline`、`tp-action` 等(`define-tp` 定义;`tp-link` 的颜色在应用时解析,主题切换即时生效) | | `tp-pop-to-buffer` / `tp-switch-to-buffer` | 显示带属性文本的缓冲辅助宏(q 绑定在缓冲区局部 minor-mode keymap 中) | | `tp-palette-show` | 展示所有调色板 | -| `tp-suffix-symbol` | 符号加后缀助手 | +| `tp--suffix-symbol` | 符号加后缀助手(0.3.0 起转为私有;`tp-suffix-symbol` 保留为废弃兼容别名) | --- ## 钩子变量:唯一许可的反向调用 -分层规则的唯一例外是四个**钩子变量**:下层模块声明变量并在需要时 `funcall`,实现由 tp-render.el 在加载时安装。这样下层模块不必 `require` 上层模块,依赖图保持严格单向;而在未加载 tp-render 时,下层模块依然可用(钩子为 nil 时优雅降级)。 +分层规则的唯一例外是两个**钩子变量**:下层模块声明变量并在需要时 `funcall`,实现由 tp-render.el 在加载时安装。这样下层模块不必 `require` 上层模块,依赖图保持严格单向;而在未加载 tp-render 时,下层模块依然可用(钩子为 nil 时优雅降级:层可以定义与应用,只是没有自动重渲染)。 | 钩子变量 | 声明于 | 安装的实现(tp-render.el) | 用途 | |----------|--------|---------------------------|------| | `tp--reactive-update-function` | tp-reactive.el | `tp--reactive-apply-update` | 变量监听器触发的重计算与重渲染 | -| `tp--reactive-flush-function` | tp-reactive.el | `tp--reactive-flush-entry` | 批量更新队列刷新时的重渲染 | | `tp--layer-refresh-function` | tp-layer.el | `tp--update-layer-regions` | 层重定义后刷新已应用区域 | -| `tp--tp-text-handler-function` | tp-ops.el | `tp--handle-tp-text-property` | `tp-set` 等操作中处理 `tp-text` 属性 | + +0.2.0 时钩子有四个;0.3.0 删掉了其中两个,代之以真实的模块内/下行调用: + +- `tp--tp-text-handler-function`(原声明于 tp-ops):整条 `tp-text` 处理链移入 tp-ops,`tp-set` 等直接调用 `tp--handle-tp-text-property`。副产品:只加载 tp-ops 的部分加载也能完成 `tp-text` 文本替换。 +- `tp--reactive-flush-function`(原声明于 tp-reactive):`tp-with-batch-updates` 与 `tp--flush-batch-updates` 移入 tp-render,刷新直接调用 `tp--reactive-flush-entry`。副产品:部分加载下批量刷新不再被静默丢弃,而是诚实地报 void-function。 + +留下的两个钩子对应真正**源自下层的事件**(变量被 set、层被重定义),无法在不打破分层的前提下改写为下行调用。 + +--- + +## 可变状态清单 + +各模块持有的可变运行时状态及其清理入口(0.3.0 全面核对): + +| 模块 | 状态 | 描述 | 清理 | +|------|------|------|------| +| tp-core | —— | **无可变状态**(仅 `tp-debug-mode`/`tp-debug-echo` 两个用户选项;调试日志写入 *tp-debug* 缓冲区,由 `tp-debug-clear` 清除) | - | +| tp-reactive | `tp-reactive-deps` | 变量 → 依赖层 注册表 | `tp-reactive-reset` | +| tp-reactive | `tp-layer-watchers` / `tp-layer-computed` / `tp-layer-data` | `:watch` / `:compute` / `:data` 注册表 | `tp-reactive-reset` | +| tp-reactive | `tp--batch-update-pending` | 批量更新队列(0.3.0 起也被 reset 清空,防止残留条目对新定义的层重放) | `tp-reactive-reset` | +| tp-reactive | `tp--layer-buffers` | 层→缓冲区注册表(哈希表,0.3.0 新增) | `tp-reactive-reset`(clrhash);单层条目随 `tp-undefine-layer`/层重定义移除;死缓冲区经 kill-buffer-hook 与惰性访问剔除 | +| tp-reactive | `tp--batch-update-active` / `tp--reactive-updating` | 动态标志(let 绑定,非持久状态) | 随作用域退出 | +| tp-layer | `tp-layer-alist` / `tp-layer-groups` / `tp-layer-transforms` | 层、层组、转换注册表 | `tp-layer-reset` | +| tp-layer | `tp--group-generated-layers` | 层组生成的层 | `tp-layer-reset` | +| tp-layer | `tp--anonymous-layer-registry` | 匿名层驻留表 | `tp-layer-reset`;单条随 `tp-undefine-layer` / `tp-gc-anonymous-layers` | +| tp-layer | `tp--anonymous-layer-counter` | 匿名层名计数器——**刻意不清零**(任何 reset 都不动它):游离字符串上残留的 `tp-anon-N` 名字永远不能与新铸层重名 | 从不 | + +`tp-reactive-reset` 移除全部变量监听器并清空上表 tp-reactive 各行;`tp-layer-reset` 先调用 `tp-reactive-reset`,再清空 tp-layer 各注册表(计数器除外)。 --- @@ -401,26 +506,31 @@ tp-set [tp-ops] ├── tp--parse-args [tp-ops] │ ├── tp--merge-duplicate-keys [tp-core] │ └── tp--resolve-props [tp-layer] - │ ├── tp-layer-props + │ ├── tp-layer-props / tp-layer-props-with-args [tp-layer] │ ├── tp--collect-reactive-symbols [tp-core] │ ├── tp--resolve-reactive-symbols [tp-core] + │ ├── tp--anonymous-layer-name-for [tp-layer]($var 匿名层驻留) │ └── tp--register-reactive-deps [tp-reactive] - ├── tp--handle-tp-text [tp-ops] - │ ╌╌▷ tp--handle-tp-text-property [tp-render](经钩子) + ├── tp--handle-tp-text-property [tp-ops](0.3.0 起同模块直接调用,不再经钩子) + │ └── tp--tp-text-transform / tp--tp-text-replace [tp-ops] ├── tp--apply-props-to-string [tp-ops](整串形式,返回新字符串) - └── set-text-properties / put-text-property(Emacs 原生,区域形式) + ├── set-text-properties / put-text-property(Emacs 原生,区域形式) + └── tp--ops-register-layer-buffer [tp-ops](缓冲区目标) + └── tp-reactive--register-layer-buffer [tp-reactive] ``` ### tp-add 调用链 ``` tp-add [tp-ops] ├── tp--parse-args [tp-ops] - ├── tp--handle-tp-text [tp-ops] ╌╌▷ tp--handle-tp-text-property [tp-render] + ├── tp--handle-tp-text-property [tp-ops](直接调用) ├── text-properties-at(Emacs 原生) ├── tp--prepend-face [tp-core](face 家族属性) │ └── tp--deep-merge-plist [tp-core] ├── tp--deep-merge-plist [tp-core](其他嵌套属性) - └── put-text-property(Emacs 原生) + ├── put-text-property(Emacs 原生) + └── tp--ops-register-layer-buffer [tp-ops] + └── tp-reactive--register-layer-buffer [tp-reactive] ``` ### define-tp 调用链 @@ -429,7 +539,7 @@ define-tp [tp-layer](宏) └── tp--define-layer-internal [tp-layer] ├── tp--parse-define-layer-args [tp-layer] ├── tp--collect-reactive-symbols [tp-core] - ├── tp--unregister-reactive-deps [tp-reactive] + ├── tp--unregister-reactive-deps [tp-reactive](连带移除旧的缓冲区注册表条目) ├── tp--ensure-reactive-variables [tp-reactive] ├── tp--register-layer-data [tp-reactive] │ └── add-variable-watcher(Emacs 原生) @@ -455,8 +565,11 @@ tp-push-layer [tp-stack] │ │ └── tp-layer-props [tp-layer] │ └── tp-group-props / tp-group-props-with-arg [tp-layer] └── tp--stack-map-region [tp-stack](裁剪到 [START, END)) - ├── tp--stack-build-props [tp-stack] - └── set-text-properties(Emacs 原生) + ├── tp--stack-props-to-list [tp-layer](解码既有栈,含隐藏层) + ├── tp--stack-build-props [tp-layer](编码新栈/渲染缓存) + ├── set-text-properties(Emacs 原生) + └── tp--stack-register-layers [tp-stack] + └── tp-reactive--register-layer-buffer [tp-reactive] ``` ### 响应式更新调用链 @@ -470,15 +583,27 @@ tp-push-layer [tp-stack] │ └── tp--set-layer-props [tp-layer] ├── tp--set-layer-props [tp-layer](深合并回层定义;setq-local 不写全局) ├── tp--update-layer-regions [tp-render](属性更新) - │ └── tp-search-map [tp-search] - │ └── put-text-property + │ └── tp--map-layer-buffers [tp-render] + │ │(只访问注册表登记的缓冲区;unknown 层回退为 + │ │ 一次学习性全扫描并登记命中缓冲区) + │ ├── tp-reactive-layer-buffers [tp-reactive] + │ ├── tp--buffer-has-layer-region-p [tp-layer](回退路径) + │ └── 每缓冲区: + │ ├── tp-search-map [tp-search] → put-text-property + │ └── tp--write-layer-through-stack-storage [tp-render] + │ └── tp--stack-props-to-list / + │ tp--stack-build-props [tp-layer] + │ (隐藏/被覆盖的层副本同步刷新) └── tp--update-reactive-text [tp-render](tp-text 文本替换) └── tp--replace-reactive-text-in-buffer [tp-render] + └── tp--edit-region-minimal-diff [tp-render] + (最小差异、先插入后删除;文本相同则完全不动缓冲区) -批量模式(tp-with-batch-updates)/ 更新中的嵌套写入: +批量模式(tp-with-batch-updates [tp-render])/ 更新中的嵌套写入: └── tp--queue-batch-update [tp-reactive](入队,不递归) - └── tp--flush-batch-updates [tp-reactive](退出批量时) - └── ╌╌▷ tp--reactive-flush-entry [tp-render](经钩子) + └── tp--flush-batch-updates [tp-render](退出批量/最外层更新结束时; + 置于 unwind-protect 清理段,重渲染抛错也会排空队列) + └── tp--reactive-flush-entry [tp-render](0.3.0 起同模块直接调用,不再经钩子) ├── tp--update-layer-regions └── tp--update-reactive-text ``` @@ -487,9 +612,9 @@ tp-push-layer [tp-stack] ## 设计原则 -1. **严格分层**:模块只允许 `require` 并调用排在它前面的模块,字节编译器强制检查依赖顺序 -2. **钩子反转**:唯一许可的"向上调用"是四个钩子变量(`tp--tp-text-handler-function`、`tp--reactive-update-function`、`tp--reactive-flush-function`、`tp--layer-refresh-function`),由 tp-render.el 统一安装实现 -3. **单一职责**:每个模块(和函数)只负责一件事 -4. **复用优先**:共享引擎(`tp--map-intervals`、`tp--stack-map-region`、`tp--pattern-apply`、`tp--replace-match-text`)承载重复逻辑,高层函数复用而非复制 -5. **统一接口**:所有核心属性函数支持相同的调用约定(整串/区域形式、层名、`$var`) -6. **响应式解耦**:tp-reactive/tp-layer/tp-ops 不依赖渲染引擎;不加载 tp-render 时钩子为 nil,各模块优雅降级 +1. **严格分层**:模块只允许 `require` 并调用排在它前面的模块,字节编译器强制检查依赖顺序;且只声明真实存在的依赖(0.3.0 移除了 tp-stack→tp-ops 的幻影依赖,tp-palette 不依赖任何 tp- 模块) +2. **钩子反转**:唯一许可的"向上调用"是两个钩子变量(`tp--reactive-update-function`、`tp--layer-refresh-function`),由 tp-render.el 统一安装实现;能改写为下行调用的反转(tp-text 链、批量刷新)已在 0.3.0 改写掉 +3. **单一职责**:每个模块(和函数)只负责一件事;一个子系统的完整生命周期住在一个模块里(匿名层的铸造/驻留/注销/GC 全在 tp-layer,层栈存储格式知识全在 tp-layer 的编解码器) +4. **复用优先**:共享引擎(`tp--map-intervals`、`tp--stack-map-region`、`tp--pattern-apply`、`tp--replace-match-text`、`tp-reactive--buffer-layer-names`)承载重复逻辑,高层函数复用而非复制 +5. **统一接口**:所有核心属性函数支持相同的调用约定(整串/区域形式、层名、`$var`、多参数层) +6. **响应式解耦**:tp-reactive/tp-layer/tp-ops 不依赖渲染引擎;不加载 tp-render 时钩子为 nil,各模块优雅降级(`tp-text` 替换自 0.3.0 起随 tp-ops 即可用);重渲染只访问层→缓冲区注册表登记的缓冲区,未知层才回退全扫描 diff --git a/tp-doctest.el b/tp-doctest.el index 669db3a..3b99ddc 100644 --- a/tp-doctest.el +++ b/tp-doctest.el @@ -564,6 +564,317 @@ (list (tp-forward-do #'upcase 'marker nil str 3) (substring-no-properties str)))) +;; ---- 0.3.0: search bounds and SUBEXP ---- +;; Compared via tp-search / tp-at accessors, not prin1 output, so the +;; property print order difference between Emacs 28 and 29+ cannot bite. +(chk "V3-match-bounds" '((10 . 14)) + (with-temp-buffer + (insert "TODO one TODO two") + (tp-match-set "TODO" '(face warning) nil 5 18))) +(chk "V3-subexp" '(((8 10 bold) (13 14 bold)) ((0 3 bold))) + (list (tp-search (tp-regexp-set "\\([0-9]+\\)px" '(face bold) + "margin: 10px 4px" nil nil 1) + 'face) + ;; group 1 does not participate in the "bar" match + (tp-search (tp-regexp-set "\\(foo\\)\\|bar" '(face bold) + "foo bar" nil nil 1) + 'face))) +(chk "V3-subexp-out-of-range" + '(:ERROR (error "Regexp \"[0-9]+\" has no group 2")) + (tp-regexp-set "[0-9]+" '(face bold) "abc 123" nil nil 2)) +(chk "V3-regexp-bounds-and-reversed" '(((1 3 bold)) ((1 3 bold))) + (list (tp-search (tp-regexp-set "a+" '(face bold) "aaaa" 1 3) 'face) + (tp-search (tp-regexp-set "a+" '(face bold) "aaaa" 3 1) 'face))) + +;; ---- 0.3.0: PREDICATE / NOT-CURRENT ---- +(chk "V3-predicate" '((3 6) ((6 11 20))) + (list (with-temp-buffer + (insert "abcdef") + (tp-set 1 3 '(size 10)) + (tp-set 3 6 '(size 20)) + (goto-char 1) + (let ((match (tp-forward 'size 15 nil 1 + (lambda (target v) (and v (> v target)))))) + (list (prop-match-beginning match) (prop-match-end match)))) + (let ((str (copy-sequence "hello world"))) + (tp-set 0 5 '(size 10) str) + (tp-set 6 11 '(size 20) str) + (tp-forward 'size 15 str 2 + (lambda (target v) (and v (> v target))))))) +(chk "V3-not-current" '(2 5) + (with-temp-buffer + (insert "one two") + (tp-set 1 4 '(mark t)) + (tp-set 5 8 '(mark t)) + (let (a b) + (goto-char 2) + (setq a (prop-match-beginning (tp-forward 'mark t))) + (goto-char 2) + (setq b (prop-match-beginning (tp-forward 'mark t nil 1 nil t))) + (list a b)))) + +;; ---- 0.3.0: multi-argument parameterized layers ---- +(chk "V3-multiarg-specs" '((:foreground "red" :background "blue") + ((:foreground "red" :background "blue") "tip") + (:foreground "white" :background "black")) + (progn + (tp-layer-reset) + (define-tp tp-colors (fg bg) + `(face (:foreground ,fg :background ,bg))) + (list (tp-at 0 'face (tp-set "hello" 'tp-colors "red" "blue")) + (let ((str (copy-sequence "hello"))) + (tp-set 0 5 '(tp-colors ("red" "blue") help-echo "tip") str) + (list (tp-at 0 'face str) (tp-at 0 'help-echo str))) + (with-temp-buffer + (insert "Hello World") + (tp-put-layer 1 10 '(tp-colors "white" "black") 0) + (tp-at 1 'face))))) +(chk "V3-multiarg-arity-error" + '(:ERROR (error "tp layer tp-colors takes 2 argument(s), got 1")) + (tp-set "hello" 'tp-colors "red")) +(chk "V3-args-introspection" + '((face (:foreground "red" :background "blue")) + (fg bg) + ((face (:foreground "white" :background "black")) (face bold))) + (progn + (define-tps tp-badge (fg bg) + `(tp-colors ,fg ,bg) + '(face bold)) + (list (tp-layer-props-with-args 'tp-colors '("red" "blue")) + (tp-layer-arglist 'tp-colors) + (tp-group-props-with-args 'tp-badge '("white" "black"))))) + +;; ---- 0.3.0: layer visibility ---- +(chk "V3-hide-reveals-below" + '(:visible base :face default :count 2 :layers (highlight base)) + (progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (list :visible (tp-at 1 'tp-name) + :face (tp-at 1 'face) + :count (tp-layer-count 1 10) + :layers (tp-layer-list 1 10))))) +(chk "V3-hide-all-bare-and-show" '((:face nil :count 2) (:background "yellow")) + (progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-hide-layer 1 10 'base) + (let ((all-hidden (list :face (tp-at 1 'face) + :count (tp-layer-count 1 10)))) + (tp-show-layer 1 10 'highlight) + (list all-hidden (tp-at 1 'face)))))) +(chk "V3-hide-run-counts" '(1 0 0) + (progn + (tp-layer-reset) + (define-tp base () '(face default)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (list (tp-hide-layer 1 10 'base) + (tp-hide-layer 1 10 'base) + (tp-hide-layer 1 10 'nonexistent))))) +(chk "V3-merge-excludes-hidden" '(:face bold :help nil :name merged) + (progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(help-echo "tip")) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-hide-layer 1 10 'layer2) + (tp-merge-layers 1 10 'merged '(layer1 layer2)) + (list :face (tp-at 1 'face) + :help (tp-at 1 'help-echo) + :name (tp-at 1 'tp-name))))) +(chk "V3-flatten-discards-hidden" '(default flat) + (progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (tp-hide-layer 1 10 'highlight) + (tp-flatten-layers 1 10 'flat) + (list (tp-at 1 'face) (tp-at 1 'tp-name))))) + +;; ---- 0.3.0: movement additions and stack introspection ---- +(chk "V3-lower-layer" '(layer2 (layer2 layer3 layer1)) + (progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + (tp-lower-layer 1 10 'layer3 1) + (list (tp-layer-top 1 10) (tp-layer-list 1 10))))) +(chk "V3-rotate-canonical" '((layer1 layer3 layer2) (layer1 layer3 layer2)) + (progn + (tp-layer-reset) + (define-tp layer1 () '(face bold)) + (define-tp layer2 () '(face italic)) + (define-tp layer3 () '(face underline)) + (list (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + (tp-rotate-layer 1 10 'up) + (tp-layer-list 1 10)) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'layer1) + (tp-push-layer 1 10 'layer2) + (tp-push-layer 1 10 'layer3) + (tp-rotate-layer 1 10 'down 2) + (tp-layer-list 1 10))))) +;; Compared via assq/plist-get per layer: the top layer's PROPS come from +;; the direct text properties, whose plist order varies on Emacs 28. +(chk "V3-layer-stack-at" '(((highlight base) (:background "yellow") default nil) + ((highlight base) (:background "yellow") default t) + nil) + (progn + (tp-layer-reset) + (define-tp base () '(face default)) + (define-tp highlight () '(face (:background "yellow"))) + (with-temp-buffer + (insert "Hello World") + (tp-push-layer 1 10 'base) + (tp-push-layer 1 10 'highlight) + (let* ((probe (lambda () + (let ((stack (tp-layer-stack-at 1))) + (list (mapcar #'car stack) + (plist-get (cdr (assq 'highlight stack)) 'face) + (plist-get (cdr (assq 'base stack)) 'face) + (plist-get (cdr (assq 'highlight stack)) + 'tp-hidden))))) + (visible (funcall probe))) + (tp-hide-layer 1 10 'highlight) + (list visible + (funcall probe) + (with-temp-buffer (insert "Hello") (tp-layer-stack-at 1))))))) +(chk "V3-put-push-noerror" '(nil nil) + (with-temp-buffer + (insert "Hello World") + (list (tp-put-layer 1 10 'no-such-layer 0 nil t) + (tp-push-layer 1 10 'no-such-layer nil t)))) + +;; ---- 0.3.0: reactive layer-buffer registry and lifecycle ---- +(defvar reg-color "red") +(chk "V3-registry-and-track" '(unknown t (reg-layer)) + (progn + (tp-layer-reset) + (define-tp reg-layer () + :props '(face (:foreground $reg-color))) + (let ((before (tp-reactive-layer-buffers 'reg-layer))) + (with-temp-buffer + (insert "Hello") + (tp-push-layer 1 6 'reg-layer) + (let ((registered (equal (tp-reactive-layer-buffers 'reg-layer) + (list (current-buffer))))) + (list before + registered + (let ((s (tp-set "hello" 'reg-layer))) + (with-temp-buffer + (insert s) + (tp-reactive-track-buffer))))))))) +(defvar tmp-color "green") +(chk "V3-gc-anonymous" '(1 nil nil) + (progn + (tp-reactive-reset) + (tp-layer-reset) + (let ((buf (generate-new-buffer "*gc-demo*"))) + (with-current-buffer buf + (insert "Hello") + (tp-set 1 6 '(face (:foreground $tmp-color)))) + (kill-buffer buf) + (let ((collected (tp-gc-anonymous-layers))) + (list (length collected) + (tp-layer-props (car collected)) + ;; string-only layers stay `unknown' and are kept + (let ((s (tp-set "hello" '(face (:foreground $tmp-color))))) + (ignore s) + (tp-gc-anonymous-layers))))))) + +;; ---- 0.3.0: minimal-diff tp-text re-rendering ---- +(defvar counter-val "0") +(chk "V3-tp-text-minimal-diff" '("count: 9 items" 105 10) + (progn + (tp-layer-reset) + (setq counter-val "0") + (define-tp counter-label () + :props '(tp-text $counter-val)) + (with-temp-buffer + (insert "count: 0 items") + (tp-set 8 9 'counter-label) + (let ((m (copy-marker 10))) ; marker on the "i" of "items" + (setq counter-val "9") + (list (buffer-substring-no-properties 1 (point-max)) + (char-after m) + (marker-position m)))))) +(chk "V3-tp-text-noop-unmodified" nil + (with-temp-buffer + (insert "count: 9 items") + (tp-set 8 9 'counter-label) + (set-buffer-modified-p nil) + (setq counter-val "9") + (buffer-modified-p))) + +;; ---- 0.3.0: ABSOLUTE coordinates and palette primaries ---- +(chk "V3-intervals-absolute" + '(((1 6 (face bold)) (6 7 nil) (7 12 (face italic))) "bold text") + (list (with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (tp-set 7 12 '(face italic)) + (tp-intervals 1 12 nil t)) + (with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (dolist (iv (tp-intervals 1 12 nil t)) + (when (eq (plist-get (nth 2 iv) 'face) 'bold) + (tp-add (nth 0 iv) (nth 1 iv) '(help-echo "bold text")))) + (tp-at 1 'help-echo)))) +(chk "V3-intervals-map-absolute" '((1 6 bold) (6 7 nil) (7 12 italic)) + (with-temp-buffer + (insert "Hello World") + (tp-set 1 6 '(face bold)) + (tp-set 7 12 '(face italic)) + (tp-intervals-map + (lambda (start end props belows) + (ignore belows) + (list start end (plist-get props 'face))) + 1 12 nil t))) +;; The resolved color depends on the frame's light/dark mode, like the +;; U-parsecolor2 assertion above. +(chk "V3-palette-primaries" '(t nil (t t t nil)) + (list (and (member (tp-palette-color 'info :fg) + '("#0969da" "#58a6ff")) + t) + (tp-palette-color 'no-such-palette :fg) + (list (tp-palette-has-p 'info) + (tp-palette-has-p 'info :fg) + (tp-palette-has-p 'info :border) + (tp-palette-has-p 'no-such-palette)))) + (princ (format "\nTOTAL: %d FAILS: %d\n" tp-doctest--total tp-doctest--fails)) (when (> tp-doctest--fails 0) (kill-emacs 1)) diff --git a/tp-ops.el b/tp-ops.el index 5670fe5..02c6d7c 100644 --- a/tp-ops.el +++ b/tp-ops.el @@ -1148,7 +1148,8 @@ Returns a new plist (does not modify the original)." OBJECT is a string or buffer; nil means the current buffer. If START and END are not provided, they default to the whole of OBJECT: 0/(length OBJECT) for strings, `point-min'/`point-max' of -OBJECT for buffers (the current buffer when OBJECT is nil)." +OBJECT for buffers (the current buffer when OBJECT is nil). +Returns nil." (interactive) (let ((beg (or start (cond ((stringp object) 0) @@ -1160,7 +1161,8 @@ OBJECT for buffers (the current buffer when OBJECT is nil)." ((bufferp object) (with-current-buffer object (point-max))) (t (point-max)))))) - (set-text-properties beg finish nil object))) + (set-text-properties beg finish nil object) + nil)) (provide 'tp-ops) ;;; tp-ops.el ends here