From 45304541ec906855e2412e15848bb8c8e7aa9d58 Mon Sep 17 00:00:00 2001 From: Jonas Bernoulli Date: Wed, 22 Apr 2026 18:53:35 +0200 Subject: [PATCH] Update to Transient v0.13.0-10-g5b2ff26f --- doc/misc/transient.texi | 378 ++++++++++---- lisp/transient.el | 1054 +++++++++++++++++++++++++-------------- 2 files changed, 946 insertions(+), 486 deletions(-) diff --git a/doc/misc/transient.texi b/doc/misc/transient.texi index 25d0e11fac7..b0eebec3071 100644 --- a/doc/misc/transient.texi +++ b/doc/misc/transient.texi @@ -25,13 +25,13 @@ General Public License for more details. @dircategory Emacs misc features @direntry -* Transient: (transient). Transient Commands. +* Transient: (transient). Transient Commands. @end direntry @finalout @titlepage @title Transient User and Developer Manual -@subtitle for version 0.12.0 +@subtitle for version 0.13.0 @author Jonas Bernoulli @page @vskip 0pt plus 1filll @@ -53,7 +53,7 @@ resource to get over that hurdle is Psionic K's interactive tutorial, available at @uref{https://github.com/positron-solutions/transient-showcase}. @noindent -This manual is for Transient version 0.12.0. +This manual is for Transient version 0.13.0. @insertcopying @end ifnottex @@ -385,7 +385,7 @@ than outlined above and even customizable.} If the user does not save the value and just exits using a regular suffix command, then the value is merely saved to the transient's history. That value won't be used when the transient is next invoked, -but it is easily accessible (@pxref{Using History}). +but it is easily accessible (see @ref{Using History}). Option @code{transient-common-command-prefix} controls the prefix key used in the following bindings. For simplicity's sake the default, @kbd{C-x}, @@ -454,8 +454,8 @@ previously used values. Usually the same keys as those mentioned above are bound to those commands. Authors of transients should arrange for different infix commands that -read the same kind of value to also use the same history key -(@pxref{Suffix Slots}). +read the same kind of value to also use the same history key (see +@ref{Suffix Slots}). Both kinds of history are saved to a file when Emacs is exited. @@ -680,7 +680,7 @@ More options are described in @ref{Common Suffix Commands}, in @ref{Saving Value Two more essential options are documented in @ref{Common Suffix Commands}. -@defopt transient-show-popup +@defopt transient-show-menu This option controls whether and when transient's menu buffer is shown. @@ -747,16 +747,23 @@ from the user. If @code{nil}, there is no initial input and the first element has to be accessed the same way as the older elements. @end defopt -@defopt transient-enable-popup-navigation +@defopt transient-enable-menu-navigation This option controls whether navigation commands are enabled in -transient's menu buffer. If the value is @code{verbose} (the default), -brief documentation about the command under point is additionally -show in the echo area. +transient menu buffer, and whether additional documentation is shown +in the echo area while doing so. -While a transient is active the menu buffer is not the current -buffer, making it necessary to use dedicated commands to act on that -buffer itself. If this option is non-@code{nil}, then the following -features are available: +If the value is @code{verbose} (the default), additional documentation +about the command at point is shown in the echo area. If this would +result in the same documentation, which is being displayed inside +the menu buffer, to be duplicated in the echo area, then @code{verbose} +forgoes doing so. Use @code{force-verbose} to echo even such documentation. +Use @code{t} to enable menu navigation without showing documentation in the +echo area. + +While a transient is active, the menu buffer is (by default) not the +current buffer, making it necessary to use dedicated commands to act +on that buffer itself. If this option is non-nil, then the +following bindings are available: @itemize @item @@ -766,15 +773,18 @@ features are available: @item @kbd{M-@key{RET}} invokes the suffix the cursor is on. @item -@kbd{mouse-1} invokes the clicked on suffix. +@kbd{mouse-1} and @kbd{mouse-2} invokes the clicked on suffix. @item @kbd{C-s} and @kbd{C-r} start isearch in the menu buffer. @end itemize -By default @kbd{M-@key{RET}} is bound to @code{transient-push-button}, instead of -@kbd{@key{RET}}, because if a transient allows the invocation of non-suffixes, -then it is likely, that you would want @kbd{@key{RET}} to do what it would do -if no transient were active." +@kbd{mouse-1} and @kbd{mouse-2} are bound in @code{transient-button-map}. +All other bindings are in @code{transient-popup-navigation-map}. + +Instead of @kbd{@key{RET}}, @kbd{M-@key{RET}} is used to invoke the suffix command at point by +default, because if a transient allows the invocation of non-suffixes, +then it is likely that the user would want the former do what it would +do if no transient were active. @end defopt @defopt transient-display-buffer-action @@ -785,7 +795,7 @@ menu buffer. The menu buffer is displayed in a window using The value of this option has the form @code{(@var{FUNCTION} . @var{ALIST})}, where @var{FUNCTION} is a function or a list of functions. Each such function should accept two arguments: a buffer to display and an -alist of the same form as @var{ALIST}. @xref{Choosing Window,,,elisp,}, +alist of the same form as @var{ALIST}. See @ref{Choosing Window,,,elisp,}, for details. The default is: @@ -798,8 +808,8 @@ The default is: @end lisp This displays the window at the bottom of the selected frame. -For alternatives @xref{Buffer Display Action Functions,,,elisp,}, -and @xref{Buffer Display Action Alists,,,elisp,}. +For alternatives see @ref{Buffer Display Action Functions,,,elisp,}, +and @ref{Buffer Display Action Alists,,,elisp,}. When you switch to a different ACTION, you should keep the ALIST entries for @code{dedicated} and @code{inhibit-same-window} in most cases. @@ -828,11 +838,147 @@ when creating a new prefix with @code{transient-define-prefix}. @anchor{Accessibility Options} @subheading Accessibility Options +For visually impaired users I recommend the following configuration. +If you need more guidance or would like to share your experience, +please don't hesitate to contact me. + +@lisp +(setopt transient-enable-menu-navigation 'force-verbose) +(setopt transient-navigate-to-group-descriptions t) +(setopt transient-describe-menu t) +(setopt transient-select-menu-window t) +(setopt transient-force-single-column t) +(setopt transient-prefer-reading-value t) +(setopt transient-use-accessible-values t) +(setopt transient-use-accessible-formats t) +@end lisp + +Additionally you have allow the command, which you use to read the +text at point, to be run when a transient menu is active, for example: + +@lisp +(define-key transient-predicate-map + [my-read-text-at-point] #'transient--do-stay) +@end lisp + +@defopt transient-enable-menu-navigation +This option is documented in the previous node (@ref{Essential Options}). You might want to change the value from @code{verbose} to +@code{force-verbose}, which causes information to be shown in the echo +area, even if it is identical to information already displayed in +the menu buffer. Whether that is useful to you depends on whether +your setup makes it easy to read the last message displayed in the +echo area. +@end defopt + +@defopt transient-navigate-to-group-descriptions +This option controls whether menu navigation commands stop at group +descriptions. If your output method works by reading the text at +point, you most likely want to enable this. + +If @code{transient-enable-menu-navigation} is non-@code{nil}, which it is by +default, @kbd{@key{UP}} and @kbd{@key{DOWN}} move from suffix to suffix. When this option +is non-@code{nil} as well, then they additionally stop at group descriptions. +This is useful because it allows braille and audio devices to output +the group title at point. +@end defopt + +@defopt transient-describe-menu +This option controls whether a short description about the menu +itself is inserted at the beginning of the menu buffer. + +When this is non-@code{nil}, then the menu buffer begins with a short +description. Ideally this is a string written exactly for that +purpose, but because this is a new feature, most menu commands do +not provide that yet. In that case the first line of the prefix +command's docstring is used as fallback. If the value is @code{docstring}, +then the docstring is used even if a description is available. +@end defopt + +@defopt transient-select-menu-window +This option controls whether the window displaying the transient menu +is automatically selected as soon as it is displayed. + +Enabling this is discouraged, except for users of braille and audio +output devises. Note that enabling this, or alternatively selecting +the menu window on demand, are both unnecessary, to be able to move +the cursor in the menu. See @code{transient-enable-menu-navigation}. +@end defopt + @defopt transient-force-single-column This option controls whether the use of a single column to display suffixes is enforced. This might be useful for users with low vision who use large text and might otherwise have to scroll in two -dimensions. +dimensions. This is also useful for blind users, because it causes +suffixes to be navigated in a more natural order, because often +related commands are displayed in the same column but navigation +first moves horizontally to the next item on the same row. +@end defopt + +@defopt transient-prefer-reading-value +This option controls whether reading a new value is preferred over +other value selection methods. + +If this is @code{nil} (the default), then certain arguments are directly +disabled when they are invoked, while they have a non-@code{nil} value. I.e., +to switch from one non-@code{nil} value to another non-@code{nil} value, such commands +have to be invoked twice. For other arguments, which happen to have a +small set of possible values, all values are displayed at all times, +using solely coloring to indicate which of the values is active. When +such an infix command is invoked it cycles to the next value. + +This default does not work for visually impaired user. If this option +is non-@code{nil}, then more arguments immediately read the new value, instead +of being toggled off on first invocation, or instead of cycling through +values. +@end defopt + +@defopt transient-use-accessible-values +This option controls whether values are shown in a way that does not +rely on coloring. + +If this is @code{nil} (the default), then colors are used to communicate the +state of arguments. For certain argument types the state is solely +communicated that way. For example, an enabled command-line switch is +shown using some bright color, and disabling that argument, changes the +color to gray, without otherwise changing the displayed text. + +This default does not work for visually impaired user. If this option +is non-@code{nil}, then the state is additionally communicated through other +means. A switch, for example, is either followed by "is enabled" or +"is disabled". How exactly the state is communicated depends on the +type of the infix command. + +Note that packages, which use Transient, can define their own infix +command types, which may or may not involve overriding Transient's +code, which honors this new option. I.e., it will take some time until +everything respects this setting. +@end defopt + +@defopt transient-use-accessible-formats +This option controls whether more accessible format strings are used +for menu elements. + +If this is non-@code{nil}, then menu elements are displayed in a way, that I +hope, is more suitable for visually impaired users than the default. +Please provide feedback, so that we can together work on improving this. + +By default the format specified by an element's @code{format} slot is used. +When this is non-@code{nil}, then the @code{accessible-format} slot is used instead. +One change implemented in the latter is that for an element representing +a command-line argument, the argument and its value are moved before the +description, giving quicker access to the current state, while still +allowing users to read the description, in case they don't know yet what +the argument in question does. + +Enabling this also causes the string "inapt" to be added at the very +beginning of the text describing a command that currently cannot be +used. When using the default format, the only visual clue that a +command is inapt, is that the complete text representing it is grayed +out. (As an example of such an inapt command, consider the case of a +commands that can only act on the file at point, when there currently +isn't a file at point.) Placing the string "inapt" at the very +beginning gives users the opportunity to immediately skip over unusable +commands, while still giving them the opportunity to read on. @end defopt @anchor{Auxiliary Options} @@ -861,7 +1007,7 @@ used to draw the line. This user option may be overridden if @code{:mode-line-format} is passed when creating a new prefix with @code{transient-define-prefix}. -Otherwise this can be any mode-line format. @xref{Mode Line Format,,,elisp,}, for details. +Otherwise this can be any mode-line format. See @ref{Mode Line Format,,,elisp,}, for details. @end defopt @defopt transient-semantic-coloring @@ -1002,8 +1148,8 @@ That buffer is current and empty when this hook is runs. @cindex modifying existing transients -To an extent, transients can be customized interactively, -@xref{Enabling and Disabling Suffixes}. This section explains how existing +To an extent, transients can be customized interactively, see +@ref{Enabling and Disabling Suffixes}. This section explains how existing transients can be further modified non-interactively. Let's begin with an example: @@ -1029,10 +1175,10 @@ which can be included in multiple prefixes. See TODO@. as expected by @code{transient-define-prefix}. Note that an infix is a special kind of suffix. Depending on context ``suffixes'' means ``suffixes (including infixes)'' or ``non-infix suffixes''. Here it -means the former. @xref{Suffix Specifications}. +means the former. See @ref{Suffix Specifications}. @var{SUFFIX} may also be a group in the same form as expected by -@code{transient-define-prefix}. @xref{Group Specifications}. +@code{transient-define-prefix}. See @ref{Group Specifications}. @item @var{LOC} is a key description (a string as returned by @code{key-description} @@ -1072,7 +1218,7 @@ or after @var{LOC}. Conceptually adding a binding to a transient prefix is similar to adding a binding to a keymap, but this is complicated by the fact that multiple suffix commands can be bound to the same key, provided -they are never active at the same time, @xref{Predicate Slots}. +they are never active at the same time, see @ref{Predicate Slots}. Unfortunately both false-positives and false-negatives are possible. To deal with the former, use non-@code{nil} @var{KEEP-OTHER@.} The symbol @code{always} @@ -1205,14 +1351,14 @@ enabled. One benefit of the Transient interface is that it remembers history not only on a global level (``this command was invoked using these arguments, and previously it was invoked using those other arguments''), but also remembers the values of individual arguments -independently. @xref{Using History}. +independently. See @ref{Using History}. After a transient prefix command is invoked, @kbd{C-h @var{KEY}} can be used to show the documentation for the infix or suffix command that @kbd{@var{KEY}} is -bound to (@pxref{Getting Help for Suffix Commands}), and infixes and +bound to (see @ref{Getting Help for Suffix Commands}), and infixes and suffixes can be removed from the transient using @kbd{C-x l @var{KEY}}. Infixes and suffixes that are disabled by default can be enabled the same way. -@xref{Enabling and Disabling Suffixes}. +See @ref{Enabling and Disabling Suffixes}. Transient ships with support for a few different types of specialized infix commands. A command that sets a command line option, for example, @@ -1263,7 +1409,7 @@ explicitly. @var{GROUP}s add key bindings for infix and suffix commands and specify how these bindings are presented in the menu buffer. At least one -@var{GROUP} has to be specified. @xref{Binding Suffix and Infix Commands}. +@var{GROUP} has to be specified. See @ref{Binding Suffix and Infix Commands}. The @var{BODY} is optional. If it is omitted, then @var{ARGLIST} is ignored and the function definition becomes: @@ -1314,13 +1460,11 @@ GROUPs have the same form as for @code{transient-define-prefix}. @section Binding Suffix and Infix Commands The macro @code{transient-define-prefix} is used to define a transient. -This defines the actual transient prefix command (@pxref{Defining -Transients}) and adds the transient's infix and suffix bindings, as +This defines the actual transient prefix command (see @ref{Defining Transients}) and adds the transient's infix and suffix bindings, as described below. Users and third-party packages can add additional bindings using -functions such as @code{transient-insert-suffix} (@pxref{Modifying Existing Transients}). -These functions take a ``suffix specification'' as one of +functions such as @code{transient-insert-suffix} (see @ref{Modifying Existing Transients}). These functions take a ``suffix specification'' as one of their arguments, which has the same form as the specifications used in @code{transient-define-prefix}. @@ -1336,7 +1480,7 @@ for a set of suffixes. Several group classes exist, some of which organize suffixes in subgroups. In most cases the class does not have to be specified -explicitly, but @xref{Group Classes}. +explicitly, but see @ref{Group Classes}. Groups are specified in the call to @code{transient-define-prefix}, using vectors. Because groups are represented using vectors, we cannot use @@ -1346,13 +1490,10 @@ brackets to do the latter. Group specifications then have this form: @lisp -[@{@var{LEVEL}@} @{@var{DESCRIPTION}@} - @{@var{KEYWORD} @var{VALUE}@}... - @var{ELEMENT}...] +[@{LEVEL@} @{DESCRIPTION@} @{KEYWORD VALUE@}... ELEMENT...] @end lisp -The @var{LEVEL} is optional and defaults to 4. @xref{Enabling and -Disabling Suffixes}. +The @var{LEVEL} is optional and defaults to 4. See @ref{Enabling and Disabling Suffixes}. The @var{DESCRIPTION} is optional. If present, it is used as the heading of the group. @@ -1383,7 +1524,7 @@ useful while rebase is already in progress; and another that uses initiate a rebase. These predicates can also be used on individual suffixes and are -only documented once, @xref{Predicate Slots}. +only documented once, see @ref{Predicate Slots}. @item The value of @code{:hide}, if non-@code{nil}, is a predicate that controls @@ -1488,13 +1629,13 @@ The form of suffix specifications is documented in the next node. @cindex suffix specifications A transient's suffix and infix commands are bound when the transient -prefix command is defined using @code{transient-define-prefix}, -@xref{Defining Transients}. The commands are organized into groups, -@xref{Group Specifications}. Here we describe the form used to bind an +prefix command is defined using @code{transient-define-prefix}, see +@ref{Defining Transients}. The commands are organized into groups, see +@ref{Group Specifications}. Here we describe the form used to bind an individual suffix command. The same form is also used when later binding additional commands -using functions such as @code{transient-insert-suffix}, @xref{Modifying Existing Transients}. +using functions such as @code{transient-insert-suffix}, see @ref{Modifying Existing Transients}. Note that an infix is a special kind of suffix. Depending on context ``suffixes'' means ``suffixes (including infixes)'' or ``non-infix @@ -1503,9 +1644,7 @@ suffixes''. Here it means the former. Suffix specifications have this form: @lisp -([@var{LEVEL}] - [@var{KEY} [@var{DESCRIPTION}]] - @var{COMMAND}|@var{ARGUMENT} [@var{KEYWORD} @var{VALUE}]...) +([LEVEL] [KEY [DESCRIPTION]] COMMAND|ARGUMENT [KEYWORD VALUE]...) @end lisp @var{LEVEL}, @var{KEY} and @var{DESCRIPTION} can also be specified using the @var{KEYWORD}s @@ -1516,8 +1655,8 @@ the object's values just for the binding inside this transient. @itemize @item -@var{LEVEL} is the suffix level, an integer between 1 and 7. -@xref{Enabling and Disabling Suffixes}. +@var{LEVEL} is the suffix level, an integer between 1 and 7. See +@ref{Enabling and Disabling Suffixes}. @item KEY is the key binding, a string in the format returned by @@ -1591,7 +1730,7 @@ guessed based on the long argument. If the argument ends with @samp{=} Finally, details can be specified using optional @var{KEYWORD}-@var{VALUE} pairs. Each keyword has to be a keyword symbol, either @code{:class} or a keyword -argument supported by the constructor of that class. @xref{Suffix Slots}. +argument supported by the constructor of that class. See @ref{Suffix Slots}. If a keyword argument accepts a function as value, you an use a @code{lambda} expression. As a special case, the @code{##} macro (which returns a @code{lambda} @@ -1941,8 +2080,8 @@ means that all outer prefixes are exited at once. @item The behavior for non-suffixes can be set for a particular prefix, by the prefix's @code{transient-non-suffix} slot to a boolean, a suitable -pre-command function, or a shorthand for such a function. -@xref{Pre-commands for Non-Suffixes}. +pre-command function, or a shorthand for such a function. See +@ref{Pre-commands for Non-Suffixes}. @item The common behavior for the suffixes of a particular prefix can be @@ -2267,7 +2406,7 @@ Transient itself provides a single class for prefix commands, @code{transient-prefix}, but package authors may wish to define specialized classes. Doing so makes it possible to change the behavior of the set of prefix commands that use that class, by implementing specialized -methods for certain generic functions (@pxref{Prefix Methods}). +methods for certain generic functions (see @ref{Prefix Methods}). A transient prefix command's object is stored in the @code{transient--prefix} property of the command symbol. While a transient is active, a clone @@ -2282,7 +2421,7 @@ object should not affect later invocations. @item All suffix and infix classes derive from @code{transient-suffix}, which in turn derives from @code{transient-child}, from which @code{transient-group} also -derives (@pxref{Group Classes}). +derives (see @ref{Group Classes}). @item All infix classes derive from the abstract @code{transient-infix} class, @@ -2290,13 +2429,13 @@ which in turn derives from the @code{transient-suffix} class. Infixes are a special type of suffixes. The primary difference is that infixes always use the @code{transient--do-stay} pre-command, while -non-infix suffixes use a variety of pre-commands (@pxref{Transient State}). Doing that is most easily achieved by using this class, +non-infix suffixes use a variety of pre-commands (see @ref{Transient State}). Doing that is most easily achieved by using this class, though theoretically it would be possible to define an infix class that does not do so. If you do that then you get to implement many methods. Also, infixes and non-infix suffixes are usually defined using -different macros (@pxref{Defining Suffix and Infix Commands}). +different macros (see @ref{Defining Suffix and Infix Commands}). @item Classes used for infix commands that represent arguments should @@ -2607,7 +2746,7 @@ returns a brief summary about the command at point or hovered with the mouse. This function is called when the mouse is moved over a command and -(if the value of @code{transient-enable-popup-navigation} is @code{verbose}) when +(if the value of @code{transient-enable-menu-navigation} is @code{verbose}) when the user navigates to a command using the keyboard. If OBJ's @code{summary} slot is a string, that is used. If @code{summary} is a @@ -2706,7 +2845,7 @@ secondary value, called a ``scope''. See @code{transient-define-prefix}. @code{transient-suffix}, @code{transient-non-suffix} and @code{transient-switch-frame} play a part when determining whether the currently active transient prefix command remains active/transient when a suffix or arbitrary -non-suffix command is invoked. @xref{Transient State}. +non-suffix command is invoked. See @ref{Transient State}. @item @code{refresh-suffixes} Normally suffix objects and keymaps are only setup @@ -2766,6 +2905,12 @@ fallback descriptions for suffixes that lack a description. This is intended to be temporarily used when implementing of a new prefix command, at which time @code{transient-command-summary-or-name} is a useful value. + +@item +@code{description} a short string describing the prefix, which users can +opt-in to be displayed at the top of the menu buffer. This should +be more concise than the first line of the docstring, which is used +as a fallback if no description is provided. @end itemize @anchor{Internal} @@ -2788,7 +2933,7 @@ of the same symbol. @item @code{level} The level of the prefix commands. The suffix commands whose -layer is equal or lower are displayed. @pxref{Enabling and Disabling Suffixes}. +layer is equal or lower are displayed. See @ref{Enabling and Disabling Suffixes}. @item @code{value} The likely outdated value of the prefix. Instead of accessing @@ -2812,19 +2957,46 @@ Here we document most of the slots that are only available for suffix objects. Some slots are shared by suffix and group objects, they are documented in @ref{Predicate Slots}. -Also @xref{Suffix Classes}. +Also see @ref{Suffix Classes}. @anchor{Slots of @code{transient-child}} @subheading Slots of @code{transient-child} -This is the abstract superclass of @code{transient-suffix} and @code{transient-group}. -This is where the shared @code{if*} and @code{inapt-if*} slots (@pxref{Predicate Slots}), -the @code{level} slot (@pxref{Enabling and Disabling Suffixes}), and the @code{advice} -and @code{advice*} slots (@pxref{Slots of @code{transient-suffix}}) are defined. +This is the abstract superclass of @code{transient-suffix} and +@code{transient-group}. In addition to the slots listed below, this class +is also where the @code{if*} and @code{inapt-if*} slots (see @ref{Predicate Slots}) and +the @code{level} slot (see @ref{Enabling and Disabling Suffixes}) are defined. @itemize @item -@code{parent} The object for the parent group. +@code{parent} The object for the parent group, if any. + +@item +@code{inactive} If an @code{:if*} predicate of a suffix or group returns @code{nil}, +then it is not displayed in the menu, but it has to remain in the +internal object tree, in case that predicate later returns @code{t}, and +the @code{object} therefore has to appear in the menu again. Likewise a +group or suffix may be (potentially only temporarily) inactive due +to its @code{level}. The @code{inactive} slot is set accordingly. Never set it +yourself. +@end itemize + +The following two slots are experimental. If they are set for a +group, then they apply to all suffixes in that group, except for +suffixes that themselves set the same slot to a non-@code{nil} value. + +@itemize +@item +@code{advice} A function used to advise the command. The advise is called +using @code{(apply advice command args)}, i.e., it behaves like an "around" +advice. + +@item +@code{advice*} A function used to advise the command. Unlike @code{advice}, this +advises not only the command body but also its @code{interactive} spec. If +both slots are non-@code{nil}, @code{advice} is used for the body and @code{advice*} is +used for the @code{interactive} form. When advising the @code{interactive} spec, +called using @code{(funcall advice #'advice-eval-interactive-spec spec)}. @end itemize @anchor{Slots of @code{transient-suffix}} @@ -2846,7 +3018,7 @@ which is useful for alignment purposes. @code{command} The command, a symbol. @item -@code{transient} Whether to stay transient. @xref{Transient State}. +@code{transient} Whether to stay transient. See @ref{Transient State}. @item @code{format} The format used to display the suffix in the menu buffer. @@ -2877,34 +3049,23 @@ unspecified, the prefix controls how help is displayed for its suffixes. See also function @code{transient-show-help}. @item -@code{summary} The summary displayed in the echo area, or as a tooltip. -If this is @code{nil}, which it usually should be, the first line of the -documentation string is used instead. See @code{transient-show-summary} -for details. +@code{summary} A short description to be displayed in addition to the text +displayed in the menu itself. If this is @code{nil}, the first line of the +documentation string is used instead. If non-@code{nil}, this must be a +string or a function that returns a string or @code{nil}. + +This description is displayed as a tooltip, when hovering an element +in the menu. If @code{transient-enable-menu-navigation} is @code{verbose}, it is +also shown in the echo area, when navigating the menu. + +The generic function @code{transient-get-summary} is used to determine and +format this description. @item @code{definition} A command, which is used if the body is omitted when defining a command using @code{transient-define-suffix}. @end itemize -The following two slots are experimental. They can also be set for a -group, in which case they apply to all suffixes in that group, except -for suffixes that set the same slot to a non-@code{nil} value. - -@itemize -@item -@code{advice} A function used to advise the command. The advise is called -using @code{(apply advice command args)}, i.e., it behaves like an "around" -advice. - -@item -@code{advice*} A function used to advise the command. Unlike @code{advice}, this -advises not only the command body but also its @code{interactive} spec. If -both slots are non-@code{nil}, @code{advice} is used for the body and @code{advice*} is -used for the @code{interactive} form. When advising the @code{interactive} spec, -called using @code{(funcall advice #'advice-eval-interactive-spec spec)}. -@end itemize - @anchor{Slots of @code{transient-infix}} @subheading Slots of @code{transient-infix} @@ -3070,14 +3231,14 @@ currently cannot be invoked. By default these predicates run when the prefix command is invoked, but this can be changes, using the @code{refresh-suffixes} prefix slot. -@xref{Prefix Slots}. +See @ref{Prefix Slots}. One more slot is shared between group and suffix classes, @code{level}. Like the slots documented above, it is a predicate, but it is used for a different purpose. The value has to be an integer between 1 and 7. @code{level} controls whether a suffix or a group should be available depending on user preference. -@xref{Enabling and Disabling Suffixes}. +See @ref{Enabling and Disabling Suffixes}. @node FAQ @appendix FAQ @@ -3092,20 +3253,17 @@ by passing @code{:display-action} to @code{transient-define-prefix}. @anchor{How can I copy text from the menu buffer?} @appendixsec How can I copy text from the menu buffer? -To be able to mark text in Transient's menu buffer using the mouse, -you have to add the below binding. Note that for technical reasons, -the region won't be visualized, while doing so. After you have quit -the transient menu, you will be able to yank it in another buffer. +You can select text in the menu buffer using the mouse, like in most +other buffers, by clicking @code{mouse-1} and keeping it pressed while +dragging. -@lisp -(keymap-set transient-predicate-map - "" - #'transient--do-stay) -@end lisp +(Before v0.13.0, the above required additional configuration and the +region was not visualized while dragging. This isn't the case anymore, +but explains why the following was once deemed necessary.) -Copying the region while not seeing the region is a bit fiddly, so a -dedicated command, @code{transient-copy-menu-text}, was added. You have to -add a binding for this command in @code{transient-map}. +Alternatively the command @code{transient-copy-menu-text} can be used to copy +the complete content of the menu buffer. You have to add a binding +for this command in @code{transient-map}. @lisp (keymap-set transient-map "C-c C-w" #'transient-copy-menu-text) @@ -3115,9 +3273,9 @@ add a binding for this command in @code{transient-map}. @appendixsec How can I autoload prefix and suffix commands? If your package only supports Emacs 30, just prefix the definition -with @code{;;;###autoload}. If your package supports released versions of -Emacs, you unfortunately have to use a long form autoload comment -as described in @ref{Autoload,,,elisp,}. +with @code{;;;###autoload}. If your package supports older Emacs releases, +you unfortunately have to use a long-form autoload comment as +described in @ref{Autoload,,,elisp,}. @lisp ;;;###autoload (autoload 'magit-dispatch "magit" nil t) diff --git a/lisp/transient.el b/lisp/transient.el index e77fef1f98a..202cd10a552 100644 --- a/lisp/transient.el +++ b/lisp/transient.el @@ -5,7 +5,7 @@ ;; Author: Jonas Bernoulli ;; URL: https://github.com/magit/transient ;; Keywords: extensions -;; Version: 0.12.0 +;; Version: 0.13.0 ;; SPDX-License-Identifier: GPL-3.0-or-later @@ -30,10 +30,16 @@ ;; in Magit. It is distributed as a separate package, so that it can be ;; used to implement similar menus in other packages. +;; You can contact the maintainer of this package by sending an email to +;; , or you can use the public issue +;; tracker at . The latter can also +;; be accessed using the `forge' package, which lets you avoid the sadly +;; non-free javascript on that website. + ;;; Code: ;;;; Frontmatter -(defconst transient-version "v0.12.0-15-gfe5214e6-builtin") +(defconst transient-version "v0.13.0-10-g5b2ff26f-builtin") (require 'cl-lib) (require 'eieio) @@ -52,6 +58,16 @@ (defvar Man-notify-method) +(define-obsolete-variable-alias + 'transient-show-popup + 'transient-show-menu + "transient 0.13.0") + +(define-obsolete-variable-alias + 'transient-enable-popup-navigation + 'transient-enable-menu-navigation + "transient 0.13.0") + (defvar transient-common-command-prefix) (defmacro transient--with-emergency-exit (id &rest body) @@ -76,7 +92,7 @@ "Transient commands." :group 'extensions) -(defcustom transient-show-popup t +(defcustom transient-show-menu t "Whether and when to show transient's menu in a buffer. \\\ @@ -100,39 +116,84 @@ (const :tag "On demand (no summary)" 0) (number :tag "After delay" 1))) -(defcustom transient-enable-popup-navigation 'verbose +(defcustom transient-enable-menu-navigation 'verbose "Whether navigation commands are enabled in the menu buffer. -If the value is `verbose' (the default), additionally show brief -documentation about the command under point in the echo area. +If the value is `verbose' (the default), show additional documentation +about the command at point in the echo area. If this would result in +the same documentation, which is being displayed inside the menu buffer, +to be duplicated in the echo area, then `verbose' forgoes doing so. +Use `force-verbose' to echo even such documentation. If `t', enable +navigation, but without echoing any documentation. -While a transient is active transient's menu buffer is not the -current buffer, making it necessary to use dedicated commands to -act on that buffer itself. If this is non-nil, then the following +While a transient is active, the menu buffer is (by default) not the +current buffer, making it necessary to use dedicated commands to act +on that buffer itself. If this option is non-nil, then the following bindings are available: \\\ - \\[transient-backward-button] moves the cursor to the previous suffix. - \\[transient-forward-button] moves the cursor to the next suffix. - \\[transient-push-button] invokes the suffix the cursor is on. -\\\ - \\`' and \\`' invoke the clicked on suffix. -\\\ - \\[transient-isearch-backward]\ and \\[transient-isearch-forward] start isearch in the menu buffer. -\\`' and \\`' are bound in `transient-push-button'. +\\`' and \\`' are bound in `transient-button-map'. All other bindings are in `transient-popup-navigation-map'. -By default \\`M-RET' is bound to `transient-push-button', instead of -\\`RET', because if a transient allows the invocation of non-suffixes, -then it is likely, that you would want \\`RET' to do what it would do -if no transient were active." +Instead of \\`RET', \\`M-RET' is used to invoke the suffix command at point by +default, because if a transient allows the invocation of non-suffixes, +then it is likely that the user would want the former do what it would +do if no transient were active." :package-version '(transient . "0.7.8") :group 'transient - :type '(choice (const :tag "Enable navigation and echo summary" verbose) - (const :tag "Enable navigation commands" t) - (const :tag "Disable navigation commands" nil))) + :type + '(choice + (const :tag "Enable navigation and force showing summary" force-verbose) + (const :tag "Enable navigation and enable showing summary" verbose) + (const :tag "Enable navigation commands" t) + (const :tag "Disable navigation commands" nil))) + +(defcustom transient-navigate-to-group-descriptions nil + "Whether menu navigation commands stop at group descriptions. + +If `transient-enable-menu-navigation' is non-nil, which it is by default, +\\\ +then \\[transient-backward-button] and \\[transient-forward-button] move \ +from suffix to suffix. When this option is +non-nil as well, then they additionally stop at group descriptions. This +is useful for visually impaired users, who use a braille or audio output +device." + :package-version '(transient . "0.13.0") + :group 'transient + :type 'boolean) + +(defcustom transient-describe-menu nil + "Whether to begin the menu buffer with a very short description. + +When this is non-nil, then the menu buffer begins with a short +description. Ideally this is a string written exactly for that +purpose, but because this is a new feature, most menu commands +do not provide that yet. In that case the first line of the prefix +command's docstring is used as fallback. If the value is `docstring', +then the docstring is used even if a description is available." + :package-version '(transient . "0.13.0") + :group 'transient + :type '(choice (const :tag "Insert description" t) + (const :tag "Insert docstring summary" docstring) + (const :tag "Do not insert description" nil))) + +(defcustom transient-select-menu-window nil + "Whether to select the window displaying the transient menu. + +Enabling this is discouraged, except for users of braille output +devises. Note that enabling this, or alternatively selecting the +menu window on demand, are both unnecessary, to be able to move +the cursor in the menu. See `transient-enable-menu-navigation'." + :package-version '(transient . "0.13.0") + :group 'transient + :type 'boolean) (defcustom transient-display-buffer-action '(display-buffer-in-side-window @@ -312,6 +373,28 @@ This command is not bound by default, see its docstring for instructions." :group 'transient :type 'boolean) +(defcustom transient-prefer-reading-value nil + "Whether to prefer reading new value over other value selection methods. + +If this is nil (the default), then certain arguments are directly +disabled when they are invoked, while they have a non-nil value. I.e., +to switch from one non-nil value to another non-nil value, such commands +have to be invoked twice. For other arguments, which happen to have a +small set of possible values, all values are displayed at all times, +using solely coloring to indicate which of the values is active. When +such an infix command is invoked it cycles to the next value. + +This default does not work for visually impaired user. If this option +is non-nil, then more arguments immediately read the new value, instead +of being toggled off on first invocation, or instead of cycling through +values. + +If you enable this, then `transient-use-accessible-values' should also +be enabled." + :package-version '(transient . "0.13.0") + :group 'transient + :type 'boolean) + (defcustom transient-highlight-mismatched-keys nil "Whether to highlight keys that do not match their argument. @@ -434,12 +517,70 @@ See also `transient-align-variable-pitch'." (defcustom transient-force-single-column nil "Whether to force use of a single column to display suffixes. -This might be useful for users with low vision who use large -text and might otherwise have to scroll in two dimensions." +This might be useful for users with low vision who use large text +and might otherwise have to scroll in two dimensions. This is also +useful for blind users, because it causes suffixes to be navigated +in a more natural order." :package-version '(transient . "0.3.6") :group 'transient :type 'boolean) +(defcustom transient-use-accessible-values nil + "Whether to show values in a way that does not rely on coloring. + +If this is nil (the default), then colors are used to communicate the +state of arguments. For certain argument types the state is solely +communicated that way. For example, an enabled command-line switch is +shown using some bright color, and disabling that argument, changes the +color to gray, without otherwise changing the displayed text. + +This default does not work for visually impaired user. If this option +is non-nil, then the state is additionally communicated through other +means. A switch, for example, is either followed by \"is enabled\" or +\"is disabled\". How exactly the state is communicated depends on the +type of the infix command. + +Note that packages, which use Transient, can define their own infix +command types, which may or may not involve overriding Transient's +code, which honors this new option. I.e., it will take some time until +everything respects this setting. + +If you enable this, then `transient-prefer-reading-value' should also +be enabled. Also consider enabling `transient-use-accessible-formats'." + :package-version '(transient . "0.13.0") + :group 'transient + :type 'boolean) + +(defcustom transient-use-accessible-formats nil + "Whether to use a more accessible format strings for menu elements. + +If this is non-nil, then menu elements are displayed in a way, that I +hope, is more suitable for visually impaired users than the default. +Please provide feedback, so that we can together work on improving this. + +By default the format specified by an element's `format' slot is used. +When this is non-nil, then the `accessible-format' slot is used instead. +One change implemented in the latter is that for an element representing +a command-line argument, the argument and its value are moved before the +description, giving quicker access to the current state, while still +allowing users to read the description, in case they don't know yet what +the argument in question does. + +Enabling this also causes the string \"inapt\" to be added at the very +beginning of the text describing a command that currently cannot be +used. When using the default format, the only visual clue that a +command is inapt, is that the complete text representing it is grayed +out. (As an example of such an inapt command, consider the case of a +commands that can only act on the file at point, when there currently +isn't a file at point.) Placing the string \"inapt\" at the very +beginning gives users the opportunity to immediately skip over unusable +commands, while still giving them the opportunity to read on. + +Also consider enabling `transient-use-accessible-values'." + :package-version '(transient . "0.13.0") + :group 'transient + :type 'boolean) + (defconst transient--max-level 7) (defconst transient--default-child-level 1) (defconst transient--default-prefix-level 4) @@ -725,9 +866,13 @@ If `transient-save-history' is nil, then do nothing." (add-hook 'kill-emacs-hook #'transient-maybe-save-history)) ;;;; Classes -;;;; Prefix +;;;;; Base -(defclass transient-prefix () +(defclass transient-object () () :abstract t) + +;;;;; Prefix + +(defclass transient-prefix (transient-object) ((prototype :initarg :prototype) (command :initarg :command) (level :initarg :level) @@ -738,9 +883,11 @@ If `transient-save-history' is nil, then do nothing." (history :initarg :history :initform nil) (history-pos :initarg :history-pos :initform 0) (history-key :initarg :history-key :initform nil) + (description :initarg :description :initform nil) (show-help :initarg :show-help :initform nil) (info-manual :initarg :info-manual :initform nil) (man-page :initarg :man-page :initform nil) + (summary :initarg :summary :initform nil) (transient-suffix :initarg :transient-suffix :initform nil) (transient-non-suffix :initarg :transient-non-suffix :initform nil) (transient-switch-frame :initarg :transient-switch-frame) @@ -764,9 +911,9 @@ When a transient prefix command is invoked, then a clone of that object is stored in the global variable `transient--prefix' and the prototype is stored in the clone's `prototype' slot.") -;;;; Suffix +;;;;; Suffix -(defclass transient-child () +(defclass transient-child (transient-object) ((parent :initarg :parent :initform nil @@ -853,7 +1000,10 @@ the prototype is stored in the clone's `prototype' slot.") (advice* :initarg :advice* :initform nil - :documentation "Advise applied to the command body and interactive spec.")) + :documentation "Advise applied to the command body and interactive spec.") + (summary + :initarg :summary + :initform nil)) "Abstract superclass for group and suffix classes. It is undefined which predicates are used if more than one `if*' @@ -866,15 +1016,15 @@ predicate slots or more than one `inapt-if*' slots are non-nil." (command :initarg :command) (transient :initarg :transient) (format :initarg :format :initform " %k %d") + (accessible-format + :initarg :accessible-format :initform "%i%k %d") (description :initarg :description :initform nil) (face :initarg :face :initform nil) - (show-help :initarg :show-help :initform nil) - (summary :initarg :summary :initform nil)) + (show-help :initarg :show-help :initform nil)) "Superclass for suffix command.") (defclass transient-information (transient-suffix) - ((format :initform " %k %d") - (key :initform " ")) + ((key :initform " ")) "Display-only information, aligned with suffix keys. Technically a suffix object with no associated command.") @@ -897,7 +1047,8 @@ Technically a suffix object with no associated command.") (reader :initarg :reader :initform nil) (prompt :initarg :prompt :initform nil) (choices :initarg :choices :initform nil) - (format :initform " %k %d (%v)")) + (format :initform " %k %d (%v)") + (accessible-format :initform "%i%k %v (%d)")) "Transient infix command." :abstract t) @@ -913,7 +1064,8 @@ Technically a suffix object with no associated command.") (defclass transient-variable (transient-infix) ((variable :initarg :variable) - (format :initform " %k %d %v")) + (format :initform " %k %d %v") + (accessible-format :initform "%i%k %v (%d)")) "Abstract superclass for infix commands that set a variable." :abstract t) @@ -942,7 +1094,7 @@ They become the value of this argument.") (target :initarg := :initform nil)) "Class used by the `transient-describe' suffix command.") -;;;; Group +;;;;; Group (defclass transient-group (transient-child) ((suffixes :initarg :suffixes :initform nil) @@ -1033,6 +1185,7 @@ to the setup function: `(lambda () (interactive) (transient-setup ',name)))) + :autoload-end (put ',name 'interactive-only ,interactive-only) (put ',name 'function-documentation ,docstr) (put ',name 'transient--prefix @@ -1040,6 +1193,7 @@ to the setup function: (transient--set-layout ',name (list ,@(mapcan (lambda (s) (transient--parse-child name s)) groups)))))) +(put 'transient-define-prefix 'autoload-macro 'expand) (defmacro transient-define-group (name &rest groups) "Define one or more groups and store them in symbol NAME. @@ -1085,10 +1239,12 @@ ARGLIST. The infix arguments are usually accessed by using ,(if (and (not body) class (oref-default class definition)) `(oref-default ',class definition) `(lambda ,arglist ,@body))) + :autoload-end (put ',name 'interactive-only ,interactive-only) (put ',name 'function-documentation ,docstr) (put ',name 'transient--suffix (,(or class 'transient-suffix) :command ',name ,@slots))))) +(put 'transient-define-suffix 'autoload-macro 'expand) (defmacro transient-augment-suffix (name &rest args) "Augment existing command NAME with a new transient suffix object. @@ -1099,8 +1255,10 @@ Similar to `transient-define-suffix' but define a suffix object only. (pcase-let ((`(,class ,slots) (transient--expand-define-args args nil 'transient-augment-suffix t))) + :autoload-end `(put ',name 'transient--suffix (,(or class 'transient-suffix) :command ',name ,@slots)))) +(put 'transient-define-infix 'autoload-macro 'expand) (defmacro transient-define-infix (name arglist &rest args) "Define NAME as a transient infix command. @@ -1425,21 +1583,21 @@ symbol property.") (defun transient--get-layout (prefix) (cond* ((bind* - (layout - (or (get prefix 'transient--layout) - ;; Migrate unparsed legacy group definition. - (condition-case-unless-debug err - (and-let* ((value (symbol-value prefix))) - (transient--set-layout - prefix - (if (and (listp value) - (or (listp (car value)) - (vectorp (car value)))) - (transient-parse-suffixes prefix value) - (list (transient-parse-suffix prefix value))))) - (error - (message "Not a legacy group definition: %s: %S" prefix err) - nil)))))) + (layout + (or (get prefix 'transient--layout) + ;; Migrate unparsed legacy group definition. + (condition-case-unless-debug err + (and-let* ((value (symbol-value prefix))) + (transient--set-layout + prefix + (if (and (listp value) + (or (listp (car value)) + (vectorp (car value)))) + (transient-parse-suffixes prefix value) + (list (transient-parse-suffix prefix value))))) + (error + (message "Not a legacy group definition: %s: %S" prefix err) + nil)))))) ((not layout) (error "Not a transient prefix command or group definition: %s" prefix)) ((vectorp layout) @@ -1452,7 +1610,12 @@ symbol property.") (transient--set-layout prefix (named-let upgrade ((spec layout)) - (cond ((vectorp spec) + (cond ((and (vectorp spec) + (length= spec 3)) + ;; This format is used by emoji.el from Emacs <= 29.4. + (pcase-let ((`[,class ,args ,children] spec)) + (vector class args (mapcar #'upgrade children)))) + ((vectorp spec) (pcase-let ((`[,level ,class ,args ,children] spec)) (when level (setq args (plist-put args :level level))) @@ -1761,6 +1924,9 @@ That buffer is current and empty when this hook runs.") (defvar transient--refreshp nil "Whether to refresh the transient completely.") +(defvar transient--pre-command nil + "The pre-command selected for `this-command'.") + (defvar transient--all-levels-p nil "Whether temporary display of suffixes on all levels is active.") @@ -1813,15 +1979,10 @@ This is bound while the suffixes are drawn in the transient buffer.") (defvar transient--history nil) -(defvar transient--scroll-commands - '(transient-scroll-up - transient-scroll-down - mwheel-scroll - scroll-bar-toolkit-scroll)) - (defvar transient--quit-commands '(transient-quit-one - transient-quit-all)) + transient-quit-all + top-level)) ;;;; Identities @@ -1917,6 +2078,7 @@ probably use this instead: (cond* (transient--pending-suffix) (transient--current-suffix) + ((bind* (this-command (advice--cd*r this-command)))) ((or transient--prefix transient-current-prefix) (let ((suffixes @@ -2084,8 +2246,7 @@ Common Suffix Commands'." (defvar-keymap transient-popup-navigation-map :doc "One of the keymaps used when menu navigation is enabled. -See `transient-enable-popup-navigation'." - "" #'transient-noop +See `transient-enable-menu-navigation'." "" #'transient-backward-button "" #'transient-forward-button "C-r" #'transient-isearch-backward @@ -2094,7 +2255,7 @@ See `transient-enable-popup-navigation'." (defvar-keymap transient-button-map :doc "One of the keymaps used when menu navigation is enabled. -See `transient-enable-popup-navigation'." +See `transient-enable-menu-navigation'." "" #'transient-push-button "" #'transient-push-button) @@ -2150,10 +2311,10 @@ of the corresponding object." "" #'transient--do-call "" #'transient--do-stay "" #'transient--do-stay - "" #'transient--do-stay - "" #'transient--do-stay - "" #'transient--do-stay - "" #'transient--do-stay + "" #'transient--do-scroll + "" #'transient--do-scroll + "" #'transient--do-scroll + "" #'transient--do-scroll "" #'transient--do-noop "" #'transient--do-move "" #'transient--do-push-button @@ -2161,6 +2322,9 @@ of the corresponding object." "" #'transient--do-move "" #'transient--do-move "" #'transient--do-move + "" #'transient--do-mouse + "" #'transient--do-mouse + "" #'transient--do-mouse "" #'transient--do-stay "" #'transient--do-stay ;; If a valid but incomplete prefix sequence is followed by @@ -2226,7 +2390,7 @@ of the corresponding object." (when-let* ((b (keymap-lookup map "-"))) (keymap-set map "" b)) (when-let* ((b (keymap-lookup map "="))) (keymap-set map "" b)) (when-let* ((b (keymap-lookup map "+"))) (keymap-set map "" b)) - (when transient-enable-popup-navigation + (when transient-enable-menu-navigation ;; `transient--make-redisplay-map' maps only over bindings that are ;; directly in the base keymap, so that cannot be a composed keymap. (set-keymap-parent @@ -2406,13 +2570,10 @@ value. Otherwise return CHILDREN as is.") (setq transient--prefix (transient--init-prefix name params)) (setq name (oref transient--prefix command))) (setq transient--refreshp (oref transient--prefix refresh-suffixes)) - (cond ((and (not transient--refreshp) layout) - (setq transient--layout layout) - (setq transient--suffixes (transient--flatten-suffixes layout))) - (t - (setq transient--suffixes nil) - (setq transient--layout (transient--init-suffixes name)) - (setq transient--suffixes (nreverse transient--suffixes)))) + (setq transient--layout + (or (and (not transient--refreshp) layout) + (transient--init-suffixes name))) + (setq transient--suffixes (transient--flatten-suffixes transient--layout)) (slot-makeunbound transient--prefix 'value)) (defun transient--init-prefix (name &optional params) @@ -2432,17 +2593,19 @@ value. Otherwise return CHILDREN as is.") (mapcan (lambda (c) (transient--init-child levels c nil)) (append (transient--get-children name) (and (not transient--editp) - (transient--get-children 'transient-common-commands)))))) + (transient--get-children + 'transient-common-commands)))))) (defun transient--flatten-suffixes (layout) - (named-let flatten ((def layout)) - (cond ((stringp def) nil) - ((cl-typep def 'transient-information) nil) - ((listp def) (mapcan #'flatten def)) - ((cl-typep def 'transient-group) - (mapcan #'flatten (oref def suffixes))) - ((cl-typep def 'transient-suffix) - (list def))))) + (nreverse + (named-let flatten ((def layout)) + (cond ((stringp def) nil) + ((cl-typep def 'transient-information) nil) + ((listp def) (mapcan #'flatten def)) + ((cl-typep def 'transient-group) + (mapcan #'flatten (oref def suffixes))) + ((cl-typep def 'transient-suffix) + (list def)))))) (defun transient--init-child (levels spec parent) (cl-etypecase spec @@ -2510,8 +2673,7 @@ value. Otherwise return CHILDREN as is.") (cond ((not (cl-typep obj 'transient-information)) (transient--init-suffix-key obj) (transient-init-scope obj) - (transient-init-value obj) - (push obj transient--suffixes))) + (transient-init-value obj))) (list obj))) (cl-defmethod transient--init-suffix-key ((obj transient-suffix)) @@ -2673,7 +2835,10 @@ value. Otherwise return CHILDREN as is.") (transient--wrap-command) (when exitp (transient--maybe-set-value 'exit) - (transient--pre-exit))))))) + (transient--pre-exit))))) + (when (and (eq (selected-window) transient--window) + (not (eq transient--pre-command 'transient--do-mouse))) + (select-window transient--original-window)))) (defun transient--pre-exit () (transient--debug 'pre-exit) @@ -2692,6 +2857,7 @@ value. Otherwise return CHILDREN as is.") (unless (eq transient--docsp 'permanent) (setq transient--docsp nil)) (setq transient--editp nil) + (setq transient--pre-command nil) (setq transient--prefix nil) (setq transient--layout nil) (setq transient--suffixes nil) @@ -2744,13 +2910,23 @@ value. Otherwise return CHILDREN as is.") (if (not transient--prefix) (funcall fn) (transient--suspend-override (bound-and-true-p edebug-active)) - (funcall fn) ; Already unwind protected. - (cond ((memq this-command '(top-level abort-recursive-edit)) - (setq transient--exitp t) - (transient--post-exit this-command) - (transient--delete-window)) - (transient--prefix - (transient--resume-override))))) + (condition-case err + (unwind-protect + (funcall fn) + (cond + ((memq this-command '(top-level abort-recursive-edit)) + (setq transient--exitp t) + (transient--post-exit this-command) + (transient--delete-window) + (transient--debug " abort recursive-edit and menu ")) + (transient--prefix + (transient--resume-override) + (transient--debug " exit recursive-edit and resumed menu")))) + (error (if (and (eq (car err) 'error) + (stringp (cadr err)) + (string-prefix-p "Abort" (cadr err))) + (message "%s" (cadr err)) + (message "transient--recursive-edit: %S" err)))))) (defmacro transient--with-suspended-override (&rest body) (let ((depth (make-symbol "depth")) @@ -2781,52 +2957,61 @@ value. Otherwise return CHILDREN as is.") (defun transient--wrap-command () (transient--load-command-if-autoload this-command) - (letrec - ((command this-command) - (suffix (transient-suffix-object this-command)) - (prefix transient--prefix) - (advice - (lambda (fn &rest args) - (interactive - (lambda (spec) - (let ((abort t)) - (unwind-protect - (prog1 (let ((debugger #'transient--exit-and-debug)) - (if-let* ((obj suffix) - (grp (oref obj parent)) - (adv (or (oref obj advice*) - (oref grp advice*)))) - (funcall - adv #'advice-eval-interactive-spec spec) - (advice-eval-interactive-spec spec))) - (setq abort nil)) - (when abort - (when-let* ((unwind (oref prefix unwind-suffix))) - (transient--debug 'unwind-interactive) - (funcall unwind command)) - (when (symbolp command) - (remove-function (symbol-function command) advice)) - (oset prefix unwind-suffix nil)))))) - (unwind-protect - (let ((debugger #'transient--exit-and-debug)) - (if-let* ((obj suffix) - (grp (oref obj parent)) - (adv (or (oref obj advice) - (oref obj advice*) - (oref grp advice) - (oref grp advice*)))) - (apply adv fn args) - (apply fn args))) - (when-let* ((unwind (oref prefix unwind-suffix))) - (transient--debug 'unwind-command) - (funcall unwind command)) - (when (symbolp command) - (remove-function (symbol-function command) advice)) - (oset prefix unwind-suffix nil))))) - (add-function :around (if (symbolp this-command) - (symbol-function this-command) - this-command) - advice '((depth . -99))))) + (letrec ((command this-command) + (suffix (transient-suffix-object this-command)) + (prefix transient--prefix) + (advice + (lambda (fn &rest args) + (interactive + (lambda (spec) + (let ((abort t)) + (unwind-protect + (prog1 (let ((debugger #'transient--exit-and-debug)) + (if-let* ((obj suffix) + (grp (oref obj parent)) + (adv (or (oref obj advice*) + (oref grp advice*)))) + (funcall + adv #'advice-eval-interactive-spec spec) + (advice-eval-interactive-spec spec))) + (setq abort nil)) + (when abort + (when-let* ((unwind (oref prefix unwind-suffix))) + (transient--debug 'unwind-interactive) + (funcall unwind command)) + (when (symbolp command) + (remove-function (symbol-function command) advice)) + (oset prefix unwind-suffix nil)))))) + (unwind-protect + (let ((debugger #'transient--exit-and-debug)) + (if-let* ((obj suffix) + (grp (oref obj parent)) + (adv (or (oref obj advice) + (oref obj advice*) + (oref grp advice) + (oref grp advice*)))) + (apply adv fn args) + (apply fn args))) + (when-let* ((unwind (oref prefix unwind-suffix))) + (transient--debug 'unwind-command) + (funcall unwind command)) + (when (symbolp command) + (remove-function (symbol-function command) advice)) + (oset prefix unwind-suffix nil))))) + (transient--advise-this-command advice))) + +(defun transient--advise-this-command (advice) + "Add ADVICE around `this-command'. +If possible add the advice to the value of `this-command' instead of +the symbol directly, so the command's identity does not get obfuscated. +For primitive and anonymous functions that isn't possible, so fall back +to advising via the symbol in those cases." + (add-function + :around (if (and (symbolp this-command) + (not (subr-primitive-p (symbol-function this-command)))) + (symbol-function this-command) + this-command) + advice '((depth . -99)))) (defun transient--premature-post-command () (and (equal (this-command-keys-vector) []) @@ -2936,25 +3121,30 @@ value. Otherwise return CHILDREN as is.") (setq transient--stack nil)) (defun transient--redisplay () - (if (or (eq transient-show-popup t) - transient--showp) - (unless - (or (memq this-command transient--scroll-commands) - (and (or (memq this-command '(mouse-drag-region - mouse-set-region)) - (equal (key-description (this-command-keys-vector)) - "")) - (and (eq (current-buffer) transient--buffer)))) - (transient--show)) - (when (and (numberp transient-show-popup) - (not (zerop transient-show-popup)) - (not transient--timer)) - (transient--timer-start)) - (transient--show-hint))) + (cond + ((or (eq transient-show-menu t) + transient--showp) + (unless (or (eq transient--pre-command 'transient--do-move) + (eq transient--pre-command 'transient--do-scroll) + (and (or (eq transient--pre-command 'transient--do-mouse) + (eq (event-basic-type last-command-event) + 'mouse-movement)) + (eq (current-buffer) transient--buffer))) + (transient--show)) + (when (and transient-select-menu-window + (not (eq (selected-window) transient--window)) + (not (eq transient--pre-command 'transient--do-mouse))) + (select-window transient--window))) + (t + (when (and (numberp transient-show-menu) + (not (zerop transient-show-menu)) + (not transient--timer)) + (transient--timer-start)) + (transient--show-hint)))) (defun transient--timer-start () (setq transient--timer - (run-at-time (abs transient-show-popup) nil + (run-at-time (abs transient-show-menu) nil (lambda () (transient--timer-cancel) (transient--show) @@ -3004,7 +3194,11 @@ identifying the exit." (transient--post-exit this-command))) (defun transient--quit-kludge (action) - (static-if (boundp 'redisplay-can-quit) ;Emacs 31 + ;; Fixing the bug that makes this kludge necessary was proposed in + ;; https://yhetil.org/emacs-bugs/m1ikl4iqtg.fsf@dancol.org/, but it + ;; does not look like that's gonna be merged any time soon. See also + ;; https://github.com/magit/transient/commit/45fbefdc5b112f0a15cd9365. + (static-if (boundp 'redisplay-can-quit) action (pcase-exhaustive action ('enable @@ -3028,18 +3222,20 @@ identifying the exit." ;;;; Pre-Commands (defun transient--call-pre-command () - (if-let* ((fn (transient--get-pre-command this-command - (this-command-keys-vector)))) - (let ((action (funcall fn))) - (when (eq action transient--exit) - (setq transient--exitp (or transient--exitp t))) - action) - (if (let ((keys (this-command-keys-vector))) - (eq (aref keys (1- (length keys))) ?\C-g)) - (setq this-command 'transient-noop) - (unless (transient--edebug-command-p) - (setq this-command 'transient-undefined))) - transient--stay)) + (cond* + ((bind-and* (fn (transient--get-pre-command + this-command (this-command-keys-vector)))) + (setq transient--pre-command fn) + (let ((action (funcall fn))) + (when (eq action transient--exit) + (setq transient--exitp (or transient--exitp t))) + action)) + ((let ((keys (this-command-keys-vector))) + (eq (aref keys (1- (length keys))) ?\C-g)) + (transient--do-noop)) + ((transient--edebug-command-p) + (transient--do-stay)) + ((transient--do-warn)))) (defun transient--get-pre-command (&optional cmd key enforce-type) (or (and (not (eq enforce-type 'non-suffix)) @@ -3050,9 +3246,13 @@ identifying the exit." (and (symbolp def) def))) (lookup-key transient--predicate-map (vector cmd)))) (and (not (eq enforce-type 'suffix)) - (transient--resolve-pre-command - (oref transient--prefix transient-non-suffix) - t)))) + (if (equal (event-basic-type last-command-event) 'mouse-movement) + ;; `this-command' is most likely the anonymous + ;; drag command defined inside `mouse-drag-track'. + 'transient--do-mouse + (transient--resolve-pre-command + (oref transient--prefix transient-non-suffix) + t))))) (defun transient--resolve-pre-command (pre &optional resolve-boolean correct) (setq pre (cond ((booleanp pre) @@ -3070,15 +3270,18 @@ identifying the exit." (defun transient--do-stay () "Call the command without exporting variables and stay transient." + (setq transient--pre-command 'transient--do-stay) transient--stay) (defun transient--do-noop () "Call `transient-noop' and stay transient." + (setq transient--pre-command 'transient--do-noop) (setq this-command 'transient-noop) transient--stay) (defun transient--do-warn () "Call `transient-undefined' and stay transient." + (setq transient--pre-command 'transient--do-warn) (setq this-command 'transient-undefined) transient--stay) @@ -3114,17 +3317,18 @@ If there is no parent prefix, then behave like `transient--do-exit'." (defun transient--do-push-button () "Call the command represented by the activated button. Use that command's pre-command to determine transient behavior." - (if (and (mouse-event-p last-command-event) - (not (eq (posn-window (event-start last-command-event)) - transient--window))) - transient--stay - (with-selected-window transient--window - (let ((pos (if (mouse-event-p last-command-event) - (posn-point (event-start last-command-event)) - (point)))) - (setq this-command (get-text-property pos 'command)) - (setq transient--current-suffix (get-text-property pos 'suffix)))) - (transient--call-pre-command))) + (with-selected-window transient--window + (cond* + ((bind-and* + (pos (if (mouse-event-p last-command-event) + (posn-point (event-start last-command-event)) + (point))) + (obj (get-text-property pos 'button-data)) + (_(cl-typep obj '(and transient-suffix (not transient-information))))) + (setq this-command (oref obj command)) + (setq transient--current-suffix obj) + (transient--call-pre-command)) + (transient--stay)))) (defun transient--do-recurse () "Call the transient prefix command, preparing for return to outer transient. @@ -3174,13 +3378,21 @@ Do not push the active transient to the transient stack." transient--exit) (defun transient--do-move () - "Call the command if `transient-enable-popup-navigation' is non-nil. + "Call the command if `transient-enable-menu-navigation' is non-nil. In that case behave like `transient--do-stay', otherwise similar to `transient--do-warn'." - (unless transient-enable-popup-navigation + (unless transient-enable-menu-navigation (setq this-command 'transient-inhibit-move)) transient--stay) +(defun transient--do-scroll () + "Call the scroll command without exporting variables and stay transient." + transient--stay) + +(defun transient--do-mouse () + "Call the mouse command without exporting variables and stay transient." + transient--stay) + (defun transient--do-minus () "Call `negative-argument' or pivot to `transient-update'. If `negative-argument' is invoked using \"-\" then preserve the @@ -3209,7 +3421,7 @@ prefix argument and pivot to `transient-update'." (put 'transient--do-minus 'transient-face 'transient-key-stay) ;;;; Commands -;;;; Noop +;;;;; Noop (defun transient-noop () "Do nothing at all." @@ -3255,9 +3467,9 @@ Please open an issue and post the shown command log." :error))) (interactive) (message "To enable use of `%s', please customize `%s'" this-original-command - 'transient-enable-popup-navigation)) + 'transient-enable-menu-navigation)) -;;;; Core +;;;;; Core (defun transient-quit-all () "Exit all transients without saving the transient stack." @@ -3285,7 +3497,7 @@ Please open an issue and post the shown command log." :error))) "Invoke the suffix command represented by this button." (interactive)) -;;;; Suspend +;;;;; Suspend (defun transient-suspend () "Suspend the current transient. @@ -3315,7 +3527,7 @@ transient is active." (t (message "No suspended transient command")))) -;;;; Help +;;;;; Help (defun transient-help (&optional interactivep) "Show help for the active transient or one of its suffixes. @@ -3338,7 +3550,7 @@ transient is active." "From a transient menu, describe something in another buffer. This command can be bound multiple times to describe different targets. -Each binding must specify the thing it describes, be setting the value +Each binding must specify the thing it describes, by setting the value of its `target' slot, using the keyword argument `:='. The `helper' slot specifies the low-level function used to describe the @@ -3355,7 +3567,7 @@ For example: (with-slots (helper target) (transient-suffix-object) (transient--display-help helper target))) -;;;; Level +;;;;; Level (defun transient-set-level (&optional command level) "Set the level of the transient or one of its suffix commands." @@ -3414,7 +3626,7 @@ For example: (setq transient--all-levels-p (not transient--all-levels-p)) (setq transient--refreshp t)) -;;;; Value +;;;;; Value (defun transient-set () "Set active transient's value for this Emacs session." @@ -3468,7 +3680,7 @@ For example: (interactive) (transient-prefix-set (oref (transient-suffix-object) set))) -;;;; Auxiliary +;;;;; Auxiliary (transient-define-suffix transient-toggle-common () "Toggle whether common commands are permanently shown." @@ -3530,7 +3742,7 @@ such as when suggesting a new feature or reporting an issue." (message "%s: %S" (key-description (this-command-keys)) arguments))) ;;;; Value -;;;; Init +;;;;; Init (cl-defgeneric transient-init-value (obj) "Set the initial value of the prefix or suffix object OBJ. @@ -3588,7 +3800,7 @@ Call `transient-default-value' but because that is a noop for (case-fold-search nil) (regexp (if (slot-exists-p obj 'argument-regexp) (oref obj argument-regexp) - (format "\\`%s\\(.*\\)" (oref obj argument))))) + (format "\\`%s\\([^z-a]*\\)\\'" (oref obj argument))))) (if (memq multi-value '(t rest)) (cdr (assoc argument value)) (let ((match (lambda (v) @@ -3605,7 +3817,7 @@ Call `transient-default-value' but because that is a noop for (car (member (oref obj argument) (oref transient--prefix value))))) -;;;; Default +;;;;; Default (cl-defgeneric transient-default-value (obj) "Return the default value.") @@ -3627,7 +3839,7 @@ that. If the slot is unbound, return nil." Doing so causes `transient-init-value' to skip setting the `value' slot." eieio--unbound) -;;;; Read +;;;;; Read (cl-defgeneric transient-infix-read (obj) "Determine the new value of the infix object OBJ. @@ -3675,6 +3887,7 @@ it\", in which case it is pointless to preserve history.)" (with-slots (value multi-value always-read allow-empty choices) obj (if (and value (not multi-value) + (not transient-prefer-reading-value) (not always-read) transient--prefix) (oset obj value nil) @@ -3682,7 +3895,9 @@ it\", in which case it is pointless to preserve history.)" (reader (oref obj reader)) (choices (if (functionp choices) (funcall choices) choices)) (prompt (transient-prompt obj)) - (value (if multi-value (string-join value ",") value)) + (value (if (and multi-value value) + (string-join value ",") + value)) (history-key (or (oref obj history-key) (oref obj command))) (transient--history (alist-get history-key transient-history)) @@ -3690,8 +3905,9 @@ it\", in which case it is pointless to preserve history.)" (eq value (car transient--history))) transient--history (cons value transient--history))) - (initial-input (and transient-read-with-initial-input - (car transient--history))) + (initial-input (or value + (and transient-read-with-initial-input + (car transient--history)))) (history (if initial-input (cons 'transient--history 1) 'transient--history)) @@ -3719,16 +3935,24 @@ it\", in which case it is pointless to preserve history.)" (cl-defmethod transient-infix-read ((obj transient-switch)) "Toggle the switch on or off." - (if (oref obj value) nil (oref obj argument))) + (prog1 (if (oref obj value) nil (oref obj argument)) + (when transient-prefer-reading-value + (message "%s is now %s" + (oref obj argument) + (if (oref obj value) "enabled" "disabled"))))) (cl-defmethod transient-infix-read ((obj transient-switches)) "Cycle through the mutually exclusive switches. The last value is \"don't use any of these switches\"." (let ((choices (mapcar (apply-partially #'format (oref obj argument-format)) (oref obj choices)))) - (if-let* ((value (oref obj value))) - (cadr (member value choices)) - (car choices)))) + (cond* + (transient-prefer-reading-value + (let ((choice (completing-read (transient-prompt obj) choices nil t))) + (if (equal choice "") nil choice))) + ((bind-and* (value (oref obj value))) + (cadr (member value choices))) + ((car choices))))) (cl-defmethod transient-infix-read ((command symbol)) "Elsewhere use the reader of the infix command COMMAND. @@ -3739,7 +3963,7 @@ stand-alone command." (transient-infix-read obj)) (error "Not a suffix command: `%s'" command))) -;;;; Readers +;;;;; Readers (defun transient-read-file (prompt _initial-input _history) "Read a file." @@ -3786,7 +4010,20 @@ stand-alone command." (when (fboundp 'org-read-date) (org-read-date 'with-time nil nil prompt default-time))) -;;;; Prompt +(defun transient-read-string-from-buffer (prompt value _) + "Switch to a new buffer to edit STRING in a recursive edit. +Like `read-string-from-buffer' but accept an additional argument as +provided by `transient-infix-read' (but ignore it). Only available +when using Emacs 29.1 or greater." + (string-edit prompt (or value "") + (lambda (edited) + (setq value edited) + (exit-recursive-edit)) + :abort-callback #'exit-recursive-edit) + (recursive-edit) + value) + +;;;;; Prompt (cl-defgeneric transient-prompt (obj) "Return the prompt to be used to read infix object OBJ's value.") @@ -3818,16 +4055,17 @@ prompt." (if (stringp prompt) prompt "[BUG: invalid prompt]: "))) - ((bind-and* - (name (or (and (slot-boundp obj 'argument) (oref obj argument)) - (and (slot-boundp obj 'variable) (oref obj variable))))) + ((bind-and* (name (or (ignore-error (invalid-slot-name unbound-slot) + (oref obj argument)) + (ignore-error (invalid-slot-name unbound-slot) + (oref obj variable))))) (if (and (stringp name) (string-suffix-p "=" name)) name (format "%s: " name))) ("[BUG: no prompt]: "))) -;;;; Set +;;;;; Set (cl-defgeneric transient-infix-set (obj value) "Set the value of infix object OBJ to VALUE.") @@ -3897,7 +4135,7 @@ See also `transient-prefix-set'.") (transient-save-value transient--prefix) (transient-set-value transient--prefix)))))) -;;;; Save +;;;;; Save (cl-defgeneric transient-save-value (obj) "Save the value of the transient prefix OBJ.") @@ -3909,7 +4147,7 @@ See also `transient-prefix-set'.") (transient-save-values) (transient--history-push obj value))) -;;;; Reset +;;;;; Reset (cl-defgeneric transient-reset-value (obj) "Clear the set and saved values of the transient prefix OBJ.") @@ -3923,7 +4161,7 @@ See also `transient-prefix-set'.") (transient--history-push obj value)) (mapc #'transient-init-value transient--suffixes)) -;;;; Get +;;;;; Get (defun transient-args (prefix) "Return the value of the transient prefix command PREFIX. @@ -4089,7 +4327,7 @@ value of the variable. I.e., this is a side-effect and does not contribute to the value of the transient." nil) -;;;; Utilities +;;;;; Utilities (defun transient-arg-value (arg args) "Return the value of ARG as it appears in ARGS. @@ -4103,14 +4341,14 @@ Append \"=\ to ARG to indicate that it is an option." (cond* ((member arg args) t) ((bind-and* - (_(string-suffix-p "=" arg)) - (match (let ((case-fold-search nil) - (re (format "\\`%s\\(?:=\\(.+\\)\\)?\\'" - (substring arg 0 -1)))) - (cl-find-if (lambda (a) - (and (stringp a) - (string-match re a))) - args)))) + (_(string-suffix-p "=" arg)) + (match (let ((case-fold-search nil) + (re (format "\\`%s\\(?:=\\(.+\\)\\)?\\'" + (substring arg 0 -1)))) + (cl-find-if (lambda (a) + (and (stringp a) + (string-match re a))) + args)))) (match-string 1 match))))) ;;;; Return @@ -4126,7 +4364,7 @@ Append \"=\ to ARG to indicate that it is an option." (oset obj return t))) ;;;; Scope -;;;; Init +;;;;; Init (cl-defgeneric transient-init-scope (obj) "Set the scope of the prefix or suffix object OBJ. @@ -4147,7 +4385,7 @@ a default implementation, which is a noop.") (cl-defmethod transient-init-scope ((_ transient-suffix)) "Noop." nil) -;;;; Get +;;;;; Get (defun transient-scope (&optional prefixes classes) "Return the scope of the active or current transient prefix command. @@ -4246,11 +4484,10 @@ have a history of their own.") (focus nil)) (setq transient--buffer (get-buffer-create transient--buffer-name)) (with-current-buffer transient--buffer - (when transient-enable-popup-navigation - (setq focus (or (button-get (point) 'command) + (when transient-enable-menu-navigation + (setq focus (or (get-text-property (point) 'button-data) (and (not (bobp)) - (button-get (1- (point)) 'command)) - (transient--heading-at-point)))) + (get-text-property (1- (point)) 'button-data))))) (erase-buffer) (transient--insert-menu setup)) (unless (window-live-p transient--window) @@ -4262,9 +4499,11 @@ have a history of their own.") (window-parameter nil 'no-other-window)))) (when (window-live-p transient--window) (with-selected-window transient--window - (set-window-parameter nil 'no-other-window t) + (unless (eq (lookup-key transient-predicate-map [other-window]) + 'transient--do-move) + (set-window-parameter nil 'no-other-window t)) (goto-char (point-min)) - (when transient-enable-popup-navigation + (when (and focus transient-enable-menu-navigation) (transient--goto-button focus)) (transient--fit-window-to-buffer transient--window))))) @@ -4278,7 +4517,7 @@ have a history of their own.") 'transient-display-buffer-action)) (transient-display-buffer-action)))) (when (and (assq 'pop-up-frame-parameters (cdr action)) - (fboundp 'buffer-line-statistics)) ; Emacs >= 28.1 + (fboundp 'buffer-line-statistics)) ; since Emacs 28.1 (setq action (copy-tree action)) (pcase-let ((`(,height ,width) (buffer-line-statistics transient--buffer)) @@ -4324,10 +4563,12 @@ have a history of their own.") (if (window-parent win) (delete-window win) (delete-frame (window-frame win) t))))) - (when remain-in-minibuffer-window - (select-window remain-in-minibuffer-window)))) - (when (buffer-live-p transient--buffer) - (kill-buffer transient--buffer)) + (with-demoted-errors "Error while exiting transient [1]: %S" + (when remain-in-minibuffer-window + (select-window remain-in-minibuffer-window))))) + (with-demoted-errors "Error while exiting transient [2]: %S" + (when (buffer-live-p transient--buffer) + (kill-buffer transient--buffer))) (setq transient--buffer nil)) (defun transient--preserve-window-p (&optional nohide) @@ -4343,7 +4584,7 @@ have a history of their own.") ;;;; Format (defun transient--format-hint () - (if (and transient-show-popup (<= transient-show-popup 0)) + (if (and transient-show-menu (<= transient-show-menu 0)) (format "%s-" (key-description (this-command-keys))) (format "%s- [%s] %s" @@ -4384,12 +4625,27 @@ have a history of their own.") (if (or (natnump format) (eq format 'line)) nil format))) (setq mode-line-buffer-identification (symbol-name (oref transient--prefix command))) - (if transient-enable-popup-navigation + (if transient-enable-menu-navigation (setq-local cursor-in-non-selected-windows 'box) (setq cursor-type nil)) (setq display-line-numbers nil) (setq show-trailing-whitespace nil) (run-hooks 'transient-setup-buffer-hook)) + (when transient-describe-menu + (let* ((command (oref transient--prefix command)) + (desc (propertize + (or (and (not (eq transient-describe-menu 'docstring)) + (oref transient--prefix description)) + (and-let* ((doc (documentation command))) + (car (split-string doc "\n"))) + (symbol-name command)) + 'face 'transient-heading))) + (when (string-suffix-p "." desc) + (setq desc (substring desc 0 -1))) + (if transient-navigate-to-group-descriptions + (insert (transient-buttonize desc transient--prefix)) + (insert desc)) + (insert "\n\n"))) (transient--insert-groups) (when (or transient--helpp transient--editp) (transient--insert-help)) @@ -4529,7 +4785,7 @@ making `transient--original-buffer' current.") "Add additional formatting if appropriate. When reading user input for this infix, then highlight it. When edit-mode is enabled, then prepend the level information. -When `transient-enable-popup-navigation' is non-nil then format +When `transient-enable-menu-navigation' is non-nil then format as a button." (let ((str (cl-call-next-method obj))) (when (and (cl-typep obj 'transient-infix) @@ -4543,31 +4799,30 @@ as a button." 'transient-enabled-suffix 'transient-disabled-suffix))) str))) - (when (and transient-enable-popup-navigation - (slot-boundp obj 'command)) - (setq str (make-text-button str nil - 'type 'transient - 'suffix obj - 'command (oref obj command)))) - str)) + (transient-buttonize str obj))) (cl-defmethod transient-format ((obj transient-infix)) "Return a string generated using OBJ's `format'. %k is formatted using `transient-format-key'. %d is formatted using `transient-format-description'. -%v is formatted using `transient-format-value'." - (format-spec (oref obj format) +%v is formatted using `transient-format-value'. +%i is formatted using `transient-format-inapt'." + (format-spec (transient--get-format obj) `((?k . ,(transient-format-key obj)) (?d . ,(transient-format-description obj)) - (?v . ,(transient-format-value obj))))) + (?v . ,(transient-format-value obj)) + (?i . ,(transient-format-inapt obj))))) + (cl-defmethod transient-format ((obj transient-suffix)) "Return a string generated using OBJ's `format'. %k is formatted using `transient-format-key'. -%d is formatted using `transient-format-description'." - (format-spec (oref obj format) +%d is formatted using `transient-format-description'. +%i is formatted using `transient-format-inapt'." + (format-spec (transient--get-format obj) `((?k . ,(transient-format-key obj)) - (?d . ,(transient-format-description obj))))) + (?d . ,(transient-format-description obj)) + (?i . ,(transient-format-inapt obj))))) (cl-defgeneric transient-format-key (obj) "Format OBJ's `key' for display and return the result.") @@ -4657,12 +4912,15 @@ and its value is returned to the caller." "Format the description by calling the next method. If the result doesn't use the `face' property at all, then apply the face `transient-heading' to the complete string." - (and-let* ((desc (transient--get-description obj))) - (cond ((oref obj inapt) - (propertize desc 'face 'transient-inapt-suffix)) - ((text-property-not-all 0 (length desc) 'face nil desc) - desc) - ((propertize desc 'face 'transient-heading))))) + (and-let* ((desc (transient--get-description obj)) + (desc (cond ((oref obj inapt) + (propertize desc 'face 'transient-inapt-suffix)) + ((text-property-not-all 0 (length desc) 'face nil desc) + desc) + ((propertize desc 'face 'transient-heading))))) + (if transient-navigate-to-group-descriptions + (transient-buttonize desc obj) + desc))) (cl-defmethod transient-format-description :around ((obj transient-suffix)) "Format the description by calling the next method. @@ -4707,55 +4965,84 @@ apply the face `transient-unreachable' to the complete string." "Format OBJ's value for display and return the result.") (cl-defmethod transient-format-value ((obj transient-suffix)) - (propertize (oref obj argument) - 'face (if (oref obj value) - (if (oref obj inapt) - 'transient-inapt-argument - 'transient-argument) - 'transient-inactive-argument))) + (with-slots (argument value inapt) obj + (concat (propertize argument 'face (transient-argument-face obj)) + (cond ((not transient-use-accessible-values) nil) + (inapt " is inapt") + (value " is enabled") + (t " is disabled"))))) (cl-defmethod transient-format-value ((obj transient-option)) - (let ((argument (prin1-to-string (oref obj argument) t))) + (let ((argument (prin1-to-string (oref obj argument) t)) + (aface (transient-argument-face obj)) + (vface (transient-value-face obj))) (if-let* ((value (oref obj value))) - (let* ((inapt (oref obj inapt)) - (aface (if inapt 'transient-inapt-argument 'transient-argument)) - (vface (if inapt 'transient-inapt-argument 'transient-value))) - (pcase-exhaustive (oref obj multi-value) - ('nil - (concat (propertize argument 'face aface) - (propertize value 'face vface))) - ((or 't 'rest) - (concat (propertize (if (string-suffix-p " " argument) - argument - (concat argument " ")) - 'face aface) - (propertize (mapconcat #'prin1-to-string value " ") - 'face vface))) - ('repeat - (mapconcat (lambda (value) - (concat (propertize argument 'face aface) - (propertize value 'face vface))) - value " ")))) - (propertize argument 'face 'transient-inactive-argument)))) + (pcase-exhaustive (oref obj multi-value) + ('nil + (concat (propertize argument 'face aface) + (propertize value 'face vface))) + ((or 't 'rest) + (concat (propertize (if (string-suffix-p " " argument) + argument + (concat argument " ")) + 'face aface) + (propertize (mapconcat #'prin1-to-string value " ") + 'face vface))) + ('repeat + (mapconcat (lambda (value) + (concat (propertize argument 'face aface) + (propertize value 'face vface))) + value " "))) + (concat (propertize (if (string-suffix-p "=" argument) + (substring argument 0 -1) + argument) + 'face aface) + (and transient-use-accessible-values " is disabled"))))) (cl-defmethod transient-format-value ((obj transient-switches)) - (with-slots (value argument-format choices) obj - (format (propertize argument-format - 'face (if value - 'transient-argument - 'transient-inactive-argument)) - (format - (propertize "[%s]" 'face 'transient-delimiter) - (mapconcat - (lambda (choice) - (propertize choice 'face - (if (equal (format argument-format choice) value) - (if (oref obj inapt) - 'transient-inapt-argument - 'transient-value) - 'transient-inactive-value))) - choices - (propertize "|" 'face 'transient-delimiter)))))) + (pcase-let (((eieio value argument-format choices) obj) + (face (transient-argument-face obj))) + (cond + ((not transient-use-accessible-values) + (format (propertize argument-format 'face face) + (format + (propertize "[%s]" 'face 'transient-delimiter) + (mapconcat + (lambda (choice) + (propertize choice 'face + (transient-value-face + obj + (equal (format argument-format choice) value)))) + choices + (propertize "|" 'face 'transient-delimiter))))) + (value (propertize value 'face face)) + ((format "No %s argument is enabled" + (propertize (string-replace "%s" "*" argument-format) + 'face face)))))) + +(cl-defmethod transient-format-inapt ((obj transient-suffix)) + "If OBJ is currently inapt, return \"inapt \", else the empty string." + (if (oref obj inapt) "inapt " "")) + +(defun transient-argument-face (obj) + (if (oref obj value) + (if (oref obj inapt) + 'transient-inapt-argument + 'transient-argument) + 'transient-inactive-argument)) + +(cl-defun transient-value-face (obj &optional (active nil sactive)) + (if (if sactive active (oref obj value)) + (if (oref obj inapt) + ;; transient-inapt-value does not exist + 'transient-inapt-argument + 'transient-value) + 'transient-inactive-value)) + +(cl-defmethod transient--get-format ((obj transient-suffix)) + (if transient-use-accessible-formats + (oref obj accessible-format) + (oref obj format))) (cl-defmethod transient--get-description ((obj transient-child)) (cond* @@ -4814,26 +5101,17 @@ apply the face `transient-unreachable' to the complete string." (length (oref suffix key)))) (oref group suffixes)))))) -(defun transient--pixel-width (string) - (save-window-excursion - (with-temp-buffer - (insert string) - (set-window-dedicated-p nil nil) - (set-window-buffer nil (current-buffer)) - (car (window-text-pixel-size - nil (line-beginning-position) (point)))))) - (defun transient--column-stops (columns) (let* ((var-pitch (or transient-align-variable-pitch (oref transient--prefix variable-pitch))) - (char-width (and var-pitch (transient--pixel-width " ")))) + (char-width (and var-pitch (string-pixel-width " ")))) (transient--seq-reductions-from (apply-partially #'+ (* 2 (if var-pitch char-width 1))) (transient--mapn (lambda (cells min) (apply #'max (if min (if var-pitch (* min char-width) min) 0) - (mapcar (if var-pitch #'transient--pixel-width #'length) cells))) + (mapcar (if var-pitch #'string-pixel-width #'length) cells))) columns (oref transient--prefix column-widths)) 0))) @@ -4939,7 +5217,8 @@ Select the help window, and make the help buffer current and return it." (cons (lambda () (setq buffer (current-buffer))) temp-buffer-window-setup-hook))) (describe-function fn) - (set-buffer buffer))) + (when buffer + (set-buffer buffer)))) (defun transient--show-manual (manual) (info manual)) @@ -5044,8 +5323,8 @@ The current level of this menu is %s, so 'face 'transient-disabled-suffix) (propertize " 0 " 'face 'transient-disabled-suffix)))))) -(cl-defgeneric transient-show-summary (obj &optional return) - "Show brief summary about the command at point in the echo area. +(cl-defgeneric transient-get-summary (obj) + "Return brief summary about the menu element at point. If OBJ's `summary' slot is a string, use that. If it is a function, call that with OBJ as the only argument and use the returned string. @@ -5056,25 +5335,29 @@ of the documentation string, if any. If RETURN is non-nil, return the summary instead of showing it. This is used when a tooltip is needed.") -(cl-defmethod transient-show-summary ((obj transient-suffix) &optional return) - (with-slots (command summary) obj - (when-let* - ((doc (cond ((functionp summary) - (funcall summary obj)) - (summary) - ((documentation command) - (car (split-string (documentation command) "\n"))))) - (_(stringp doc)) - (_(not (equal doc - (car (split-string (documentation - 'transient--default-infix-command) - "\n")))))) - (when (string-suffix-p "." doc) - (setq doc (substring doc 0 -1))) - (if return - doc - (let ((message-log-max nil)) - (message "%s" doc)))))) +(cl-defmethod transient-get-summary ((obj transient-object)) + (cond* + ((bind-and* + (summary (cond* + ((bind* (summary (oref obj summary)))) + ((functionp summary) + (funcall summary obj)) + (summary) + ((bind-and* + (command (ignore-error (invalid-slot-name unbound-slot) + (oref obj command))) + (_(documentation command))) + (car (split-string (documentation command) "\n"))))) + (_(stringp summary)) + (_(not (equal summary + (car (split-string (documentation + 'transient--default-infix-command) + "\n")))))) + (if (string-suffix-p "." summary) + (substring summary 0 -1) + summary)) + ((eq transient-enable-menu-navigation 'force-verbose) + (transient--get-description obj)))) ;;;; Menu Navigation @@ -5100,8 +5383,7 @@ See `backward-button' for information about N." (interactive "p") (with-selected-window transient--window (backward-button n t) - (when (eq transient-enable-popup-navigation 'verbose) - (transient-show-summary (get-text-property (point) 'suffix))))) + (transient--button-move-echo))) (defun transient-forward-button (n) "Move to the next button in transient's menu buffer. @@ -5109,44 +5391,60 @@ See `forward-button' for information about N." (interactive "p") (with-selected-window transient--window (forward-button n t) - (when (eq transient-enable-popup-navigation 'verbose) - (transient-show-summary (get-text-property (point) 'suffix))))) + (transient--button-move-echo))) + +(defun transient--button-move-echo () + (when-let* ((_(memq transient-enable-menu-navigation '(verbose force-verbose))) + (obj (get-text-property (point) 'button-data))) + (let ((message-log-max nil)) + (message "%s" (or (transient-get-summary obj) ""))))) + +(defun transient--button-help-echo (win buf pos) + (with-selected-window win + (with-current-buffer buf + (transient-get-summary (get-text-property pos 'button-data))))) (define-button-type 'transient 'face nil 'keymap transient-button-map - 'help-echo (lambda (win buf pos) - (with-selected-window win - (with-current-buffer buf - (transient-show-summary - (get-text-property pos 'suffix) t))))) + 'help-echo #'transient--button-help-echo) -(defun transient--goto-button (command) - (cond - ((stringp command) - (when (re-search-forward (concat "^" (regexp-quote command)) nil t) - (goto-char (match-beginning 0)))) - (command - (cl-flet ((found () - (and-let* ((button (button-at (point)))) - (eq (button-get button 'command) command)))) - (while (and (ignore-errors (forward-button 1)) - (not (found)))) - (unless (found) +(defun transient-buttonize (string object) + (if transient-enable-menu-navigation + (make-text-button string nil 'type 'transient 'button-data object) + string)) + +(defun transient--goto-button (object) + (cl-etypecase object + (transient-prefix (goto-char (point-min))) + ((or transient-group transient-suffix) + (let ((found (transient--match-button object))) + (while (and (not (funcall found)) + (ignore-errors (forward-button 1)))) + (unless (funcall found) (goto-char (point-min)) (ignore-errors (forward-button 1)) - (unless (found) + (unless (funcall found) (goto-char (point-min)))))))) -(defun transient--heading-at-point () - (and (eq (get-text-property (point) 'face) 'transient-heading) - (let ((beg (line-beginning-position))) - (buffer-substring-no-properties - beg (next-single-property-change - beg 'face nil (line-end-position)))))) +(defun transient--match-button (object) + (cl-etypecase object + ((or transient-group transient-information) + (let ((description (transient-format-description object))) + (lambda () + (let ((obj (get-text-property (point) 'button-data))) + (and (cl-typep obj '(or transient-group transient-information)) + (equal (string-trim-left (button-label (button-at (point)))) + description)))))) + (transient-suffix + (let ((key (oref object key))) + (lambda () + (let ((obj (get-text-property (point) 'button-data))) + (and (cl-typep obj 'transient-suffix) + (eq (oref obj key) key)))))))) ;;;; Compatibility -;;;; Menu Isearch +;;;;; Menu Isearch (defvar-keymap transient--isearch-mode-map :parent isearch-mode-map @@ -5204,14 +5502,14 @@ search instead." (select-window transient--original-window) (transient--resume-override)) -;;;; Edebug +;;;;; Edebug (defun transient--edebug-command-p () (and (bound-and-true-p edebug-active) (or (memq this-command '(top-level abort-recursive-edit)) (string-prefix-p "edebug" (symbol-name this-command))))) -;;;; Miscellaneous +;;;;; Miscellaneous (cl-pushnew (list nil (concat "^\\s-*(" (eval-when-compile @@ -5226,14 +5524,13 @@ search instead." lisp-imenu-generic-expression :test #'equal) (defun transient--suspend-text-conversion-style () - (static-if (boundp 'overriding-text-conversion-style) ; since Emacs 30.1 - (when text-conversion-style - (letrec ((suspended overriding-text-conversion-style) - (fn (lambda () - (setq overriding-text-conversion-style nil) - (remove-hook 'transient-exit-hook fn)))) - (setq overriding-text-conversion-style suspended) - (add-hook 'transient-exit-hook fn))))) + (when text-conversion-style + (letrec ((suspended overriding-text-conversion-style) + (fn (lambda () + (setq overriding-text-conversion-style nil) + (remove-hook 'transient-exit-hook fn)))) + (setq overriding-text-conversion-style suspended) + (add-hook 'transient-exit-hook fn)))) (declare-function which-key-mode "ext:which-key" (&optional arg)) @@ -5318,7 +5615,7 @@ as stand-in for elements of exhausted lists." (font-lock-add-keywords 'emacs-lisp-mode transient-font-lock-keywords) ;;;; Auxiliary Classes -;;;; `transient-lisp-variable' +;;;;; `transient-lisp-variable' (defclass transient-lisp-variable (transient-variable) ((reader :initform #'transient-lisp-variable--reader) @@ -5351,10 +5648,11 @@ as stand-in for elements of exhausted lists." (defun transient-lisp-variable--reader (prompt initial-input _history) (read--expression prompt initial-input)) -;;;; `transient-cons-option' +;;;;; `transient-cons-option' (defclass transient-cons-option (transient-option) - ((format :initform " %k %d: %v")) + ((format :initform " %k %d: %v") + (accessible-format :initform "%i%k %d is %v")) "[Experimental] Class used for unencoded key-value pairs.") (cl-defmethod transient-infix-value ((obj transient-cons-option)) @@ -5370,9 +5668,11 @@ as stand-in for elements of exhausted lists." description)))) (cl-defmethod transient-format-value ((obj transient-cons-option)) - (let ((value (oref obj value))) - (propertize (prin1-to-string value t) 'face - (if value 'transient-value 'transient-inactive-value)))) + (with-slots (value) obj + (if (or value (not transient-use-accessible-values)) + (propertize (prin1-to-string value t) + 'face (transient-argument-face obj)) + "unset"))) ;;;; _ (provide 'transient) @@ -5380,6 +5680,8 @@ as stand-in for elements of exhausted lists." ;; checkdoc-symbol-words: ("command-line" "edit-mode" "help-mode") ;; indent-tabs-mode: nil ;; lisp-indent-local-overrides: ( +;; (bind* . 0) +;; (bind-and* . 0) ;; (cond . 0) ;; (cond* . 0) ;; (interactive . 0))